zlwu 6 mesi fa
parent
commit
ad9902cab6
42 ha cambiato i file con 1817 aggiunte e 163 eliminazioni
  1. 45 0
      .history/.history/backend/.env_20260422185247_20260422185246
  2. 45 0
      .history/.history/backend/.env_20260422185247_20260423140727
  3. 9 0
      .history/.history/sqxp-wx/.env_20260422212056_20260422212055.development
  4. 9 0
      .history/.history/sqxp-wx/.env_20260422212056_20260423140727.development
  5. 9 0
      .history/.history/sqxp-wx/.env_20260422212120_20260422212119.development
  6. 9 0
      .history/.history/sqxp-wx/.env_20260422212120_20260423140727.development
  7. 2 2
      .history/backend/.env_20260422185247
  8. 5 0
      .history/miniprogram/.env_20260422113918.development
  9. 5 0
      .history/miniprogram/.env_20260423152736.development
  10. 109 0
      .history/miniprogram/README_20260423140939.md
  11. 100 0
      .history/miniprogram/src/config/api_20260423152855.ts
  12. 89 0
      .history/miniprogram/src/manifest_20260423152855.json
  13. 2 2
      .history/sqxp-wx/.env_20260422212056.development
  14. 2 2
      .history/sqxp-wx/.env_20260422212120.development
  15. 9 0
      .history/sqxp-wx/.env_20260423140727.development
  16. 36 0
      .history/sqxp-wx/config/api_20260423140939.js
  17. 36 0
      .history/sqxp-wx/config/api_20260423141341.js
  18. 168 0
      .history/web-admin/vite.config_20260423140939.ts
  19. 78 0
      backend/app/api/mini_client_extra.py
  20. 1 1
      miniprogram/README.md
  21. 1 1
      miniprogram/src/config/api.ts
  22. 1 1
      miniprogram/src/manifest.json
  23. 246 14
      sqxp-wx/components/client/product-detail-overlay.vue
  24. 4 4
      sqxp-wx/config/api.js
  25. 1 0
      sqxp-wx/pages.json
  26. 13 4
      sqxp-wx/pages/client/address/address.vue
  27. 2 2
      sqxp-wx/pages/client/address/edit.vue
  28. 171 26
      sqxp-wx/pages/client/cart/cart.vue
  29. 21 6
      sqxp-wx/pages/client/home/home.vue
  30. 27 1
      sqxp-wx/pages/client/invoice/invoice.vue
  31. 387 65
      sqxp-wx/pages/client/order/detail/detail.vue
  32. 60 13
      sqxp-wx/pages/client/order/order.vue
  33. 29 1
      sqxp-wx/pages/client/order/pay/pay.vue
  34. 1 1
      sqxp-wx/pages/client/profile/profile.vue
  35. 23 1
      sqxp-wx/pages/client/refund/refund.vue
  36. 20 1
      sqxp-wx/pages/client/settlement/settlement.vue
  37. BIN
      sqxp-wx/static/tabbar/购物车-active.png
  38. BIN
      sqxp-wx/static/tabbar/购物车.png
  39. 23 11
      sqxp-wx/stores/user.js
  40. 5 0
      sqxp-wx/styles/index.scss
  41. 12 2
      sqxp-wx/utils/request.js
  42. 2 2
      web-admin/vite.config.ts

+ 45 - 0
.history/.history/backend/.env_20260422185247_20260422185246

@@ -0,0 +1,45 @@
+# 数据库连接配置(请勿提交到公共仓库;本文件已在 .gitignore 中忽略)
+# DATABASE_URL 格式:mysql+pymysql://user:password@host:port/dbname
+DATABASE_URL=mysql+pymysql://root:hn02le.34lkdLKD@47.99.164.66:3306/sanqiang
+
+# 基础配置
+APP_NAME=叁强鲜配
+DEBUG=true
+API_V1_PREFIX=/api
+
+# JWT 配置(建议在生产环境替换为高强度随机值)
+#SECRET_KEY=replace-with-strong-secret-key
+#ALGORITHM=HS256
+#ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# MinIO 配置
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=47.121.120.79:20004
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=minioadmin
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://www.chengyantek.cn/sanqiang
+
+# Redis 配置
+REDIS_HOST=47.99.164.66
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=Hzdyjs@123456
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付配置(小程序 JSAPI)
+# 按商户平台实际值填写以下 4 项
+# 微信支付商户号(mchid):微信支付商户平台 -> 账户中心 -> 商户信息 中获取(通常是 10 位数字)
+WXPAY_MCHID=1111210785
+# 小程序 AppID:微信公众平台 -> 小程序 -> 开发 -> 开发设置 中获取(格式通常 wx 开头)
+WXPAY_MINI_APPID=wxa261ad85f2c99b26
+# 小程序 AppSecret:微信公众平台 -> 小程序 -> 开发 -> 开发设置 -> 开发者ID 中获取(注意保密)
+WXPAY_SECRET=e45ccdcf034d027f182f78f177df42b6
+# 微信支付 APIv2 密钥(API Key):微信支付商户平台 -> 账户中心 -> API安全 -> 设置 APIv2 密钥
+WXPAY_API_KEY=K8vP2mQ7xN4rT9bL3cF6wJ1yH5uD0sAe
+WXPAY_SANDBOX=false
+# 回调地址(已按生产域名配置)
+WXPAY_NOTIFY_URL=https://www.chengyantek.cn/sqxp-api/payment/notify
+WXPAY_REFUND_NOTIFY_URL=https://www.chengyantek.cn/sqxp-api/payment/refund-notify

+ 45 - 0
.history/.history/backend/.env_20260422185247_20260423140727

@@ -0,0 +1,45 @@
+# 数据库连接配置(请勿提交到公共仓库;本文件已在 .gitignore 中忽略)
+# DATABASE_URL 格式:mysql+pymysql://user:password@host:port/dbname
+DATABASE_URL=mysql+pymysql://root:hn02le.34lkdLKD@47.99.164.66:3306/sanqiang
+
+# 基础配置
+APP_NAME=叁强鲜配
+DEBUG=true
+API_V1_PREFIX=/api
+
+# JWT 配置(建议在生产环境替换为高强度随机值)
+#SECRET_KEY=replace-with-strong-secret-key
+#ALGORITHM=HS256
+#ACCESS_TOKEN_EXPIRE_MINUTES=43200
+
+# MinIO 配置
+STORAGE_TYPE=minio
+MINIO_ENDPOINT=47.121.120.79:20004
+MINIO_ACCESS_KEY=minioadmin
+MINIO_SECRET_KEY=minioadmin
+MINIO_BUCKET=sanqiang
+MINIO_SECURE=false
+MINIO_BASE_URL=https://www.chengyantek.cn/sanqiang
+
+# Redis 配置
+REDIS_HOST=47.99.164.66
+REDIS_PORT=16379
+REDIS_DB=8
+REDIS_PASSWORD=Hzdyjs@123456
+REDIS_TIMEOUT_SEC=30
+REDIS_SSL_ENABLED=false
+
+# 微信支付配置(小程序 JSAPI)
+# 按商户平台实际值填写以下 4 项
+# 微信支付商户号(mchid):微信支付商户平台 -> 账户中心 -> 商户信息 中获取(通常是 10 位数字)
+WXPAY_MCHID=1111210785
+# 小程序 AppID:微信公众平台 -> 小程序 -> 开发 -> 开发设置 中获取(格式通常 wx 开头)
+WXPAY_MINI_APPID=wxa261ad85f2c99b26
+# 小程序 AppSecret:微信公众平台 -> 小程序 -> 开发 -> 开发设置 -> 开发者ID 中获取(注意保密)
+WXPAY_SECRET=e45ccdcf034d027f182f78f177df42b6
+# 微信支付 APIv2 密钥(API Key):微信支付商户平台 -> 账户中心 -> API安全 -> 设置 APIv2 密钥
+WXPAY_API_KEY=K8vP2mQ7xN4rT9bL3cF6wJ1yH5uD0sAe
+WXPAY_SANDBOX=false
+# 回调地址(已按生产域名配置)
+WXPAY_NOTIFY_URL=http://192.168.2.110:8000/apipayment/notify
+WXPAY_REFUND_NOTIFY_URL=http://192.168.2.110:8000/apipayment/refund-notify

+ 9 - 0
.history/.history/sqxp-wx/.env_20260422212056_20260422212055.development

@@ -0,0 +1,9 @@
+# sqxp-wx 开发/测试环境
+VITE_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VUE_APP_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VITE_API_BASE_URL=https://www.chengyantek.cn/sqxp-api/
+VUE_APP_API_BASE_URL=https://www.chengyantek.cn/sqxp-api/
+
+# API 应用层加密
+VITE_ENABLE_API_CRYPTO=false
+VUE_APP_ENABLE_API_CRYPTO=false

+ 9 - 0
.history/.history/sqxp-wx/.env_20260422212056_20260423140727.development

@@ -0,0 +1,9 @@
+# sqxp-wx 开发/测试环境
+VITE_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VUE_APP_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VITE_API_BASE_URL=http://192.168.2.110:8000/api
+VUE_APP_API_BASE_URL=http://192.168.2.110:8000/api
+
+# API 应用层加密
+VITE_ENABLE_API_CRYPTO=false
+VUE_APP_ENABLE_API_CRYPTO=false

+ 9 - 0
.history/.history/sqxp-wx/.env_20260422212120_20260422212119.development

@@ -0,0 +1,9 @@
+# sqxp-wx 开发/测试环境
+VITE_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VUE_APP_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VITE_API_BASE_URL=https://www.chengyantek.cn/sqxp-api/
+VUE_APP_API_BASE_URL=https://www.chengyantek.cn/sqxp-api/
+
+# API 应用层加密
+VITE_ENABLE_API_CRYPTO=false
+VUE_APP_ENABLE_API_CRYPTO=false

+ 9 - 0
.history/.history/sqxp-wx/.env_20260422212120_20260423140727.development

@@ -0,0 +1,9 @@
+# sqxp-wx 开发/测试环境
+VITE_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VUE_APP_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VITE_API_BASE_URL=http://192.168.2.110:8000/api
+VUE_APP_API_BASE_URL=http://192.168.2.110:8000/api
+
+# API 应用层加密
+VITE_ENABLE_API_CRYPTO=false
+VUE_APP_ENABLE_API_CRYPTO=false

+ 2 - 2
.history/backend/.env_20260422185247

@@ -41,5 +41,5 @@ WXPAY_SECRET=e45ccdcf034d027f182f78f177df42b6
 WXPAY_API_KEY=K8vP2mQ7xN4rT9bL3cF6wJ1yH5uD0sAe
 WXPAY_SANDBOX=false
 # 回调地址(已按生产域名配置)
-WXPAY_NOTIFY_URL=https://www.chengyantek.cn/sqxp-api/payment/notify
-WXPAY_REFUND_NOTIFY_URL=https://www.chengyantek.cn/sqxp-api/payment/refund-notify
+WXPAY_NOTIFY_URL=http://192.168.2.110:8000/apipayment/notify
+WXPAY_REFUND_NOTIFY_URL=http://192.168.2.110:8000/apipayment/refund-notify

+ 5 - 0
.history/miniprogram/.env_20260422113918.development

@@ -0,0 +1,5 @@
+# 开发 / 测试环境:远程静态资源根(须 https,以 / 结尾)
+# 与 src/config/static-assets-defaults.ts 中 STATIC_ASSETS_BASE_URL_TEST 应对齐
+VITE_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VITE_API_BASE_URL=http://192.168.2.105:8000
+VITE_ENABLE_API_CRYPTO=false

+ 5 - 0
.history/miniprogram/.env_20260423152736.development

@@ -0,0 +1,5 @@
+# 开发 / 测试环境:远程静态资源根(须 https,以 / 结尾)
+# 与 src/config/static-assets-defaults.ts 中 STATIC_ASSETS_BASE_URL_TEST 应对齐
+VITE_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VITE_API_BASE_URL=http://192.168.2.110:8000
+VITE_ENABLE_API_CRYPTO=false

+ 109 - 0
.history/miniprogram/README_20260423140939.md

@@ -0,0 +1,109 @@
+# 陇耘鲜达小程序端
+
+## 项目简介
+
+陇耘鲜达小程序端是基于 uni-app + Vue3 + TypeScript 的跨平台小程序,支持微信小程序、H5 等多端运行。
+
+## 技术栈
+
+- **框架**:uni-app + Vue3 + TypeScript
+- **UI 组件**:Vant Weapp
+- **状态管理**:Pinia
+- **样式**:SCSS
+
+## 角色说明
+
+### 客户(Client)
+- **主题色**:品牌绿 `#07c160`(三端统一)
+- **主要功能**:商品浏览、购物车、订单管理、支付、发票、结算退款等
+
+### 供应商(Supplier)
+- **主题色**:品牌绿 `#07c160`(三端统一)
+- **主要功能**:询价管理、报价管理、价格清单、配送任务(取货确认)等
+
+### 配送员(Deliverer)
+- **主题色**:品牌绿 `#07c160`(三端统一)
+- **主要功能**:配送任务管理、抢单、凭证上传、数据统计等
+
+## 目录结构
+
+```
+miniprogram/
+├── src/
+│   ├── api/              # API 接口
+│   ├── components/        # 公共组件
+│   ├── config/           # 配置文件
+│   ├── pages/            # 页面
+│   │   ├── client/       # 客户页面
+│   │   ├── supplier/     # 供应商页面
+│   │   ├── deliverer/     # 配送员页面
+│   │   └── login/         # 登录页面
+│   ├── stores/           # Pinia 状态管理
+│   ├── styles/           # 全局样式
+│   ├── types/            # TypeScript 类型定义
+│   ├── utils/            # 工具函数
+│   ├── App.vue           # 应用入口
+│   ├── main.ts          # 主入口文件
+│   └── pages.json       # 页面路由配置
+├── static/               # 静态资源
+├── index.html            # H5 入口
+├── manifest.json         # 应用配置
+├── package.json          # 依赖配置
+├── tsconfig.json         # TypeScript 配置
+└── vite.config.ts       # Vite 配置
+```
+
+## 安装依赖
+
+```bash
+npm install
+```
+
+## 开发运行
+
+```bash
+# 微信小程序
+npm run dev:mp-weixin
+
+# H5
+npm run dev:h5
+```
+
+## 构建
+
+```bash
+# 微信小程序
+npm run build:mp-weixin
+
+# H5
+npm run build:h5
+```
+
+## 登录说明
+
+### 账号密码登录
+- 使用后端分配的账号密码登录
+- 支持客户、供应商、配送员三种角色
+
+### 微信一键登录
+- 通过微信授权获取手机号
+- 自动创建或绑定用户账号
+
+## 角色路由说明
+
+登录后根据用户角色自动跳转到对应首页:
+- `client` → `/pages/client/home/home`
+- `supplier` → `/pages/supplier/home/home`
+- `deliverer` → `/pages/deliverer/home/home`
+
+## API 配置
+
+修改 `src/config/api.ts` 中的 `BASE_URL` 为实际的接口地址:
+- 开发环境:`http://192.168.2.110:8000/api`
+- 生产环境:`https://api.sanqiang.com`
+
+## 注意事项
+
+1. 确保后端服务已启动
+2. 微信小程序需要在微信公众平台配置合法域名
+3. 部分功能需要用户授权(如位置信息)

+ 100 - 0
.history/miniprogram/src/config/api_20260423152855.ts

@@ -0,0 +1,100 @@
+// ============================================
+// 陇耘鲜达小程序 - API 配置
+// ============================================
+
+// API 基础地址
+export const BASE_URL = process.env.NODE_ENV === 'development'
+  ? 'http://192.168.2.110:8000'
+  : 'https://api.sanqiang.com'
+
+// API 前缀
+export const API_PREFIX = '/api'
+
+// 完整 API 地址
+export const API_BASE = BASE_URL + API_PREFIX
+
+// 完整接口地址
+export const API_URL = {
+  // 认证相关
+  LOGIN: `${API_BASE}/mini/auth/login`,
+  SMS_LOGIN: `${API_BASE}/mini/auth/sms-login`,
+  BIND_WECHAT: `${API_BASE}/mini/auth/bind-wechat`,
+  CODE_OPENID: `${API_BASE}/mini/auth/code-openid`,
+  LOGOUT: `${API_BASE}/mini/auth/logout`,
+  REFRESH_TOKEN: `${API_BASE}/web/auth/refresh-token`,
+  CURRENT_USER: `${API_BASE}/mini/auth/current-user`,
+
+  // 用户相关
+  CHANGE_PASSWORD: `${API_BASE}/mini/user/change-password`,
+  CLIENT_PROFILE: `${API_BASE}/mini/client/profile`,
+  SUPPLIER_PROFILE: `${API_BASE}/mini/supplier/profile`,
+  DELIVERER_PROFILE: `${API_BASE}/mini/deliverer/profile`,
+
+  // 地址相关
+  CLIENT_ADDRESSES: `${API_BASE}/mini/client/addresses`,
+  SUPPLIER_ADDRESSES: `${API_BASE}/mini/supplier/addresses`,
+  SUPPLIER_PICKUP_ADDRESSES: `${API_BASE}/mini/supplier/pickup-addresses`,
+
+  // 商品相关
+  CLIENT_PRODUCTS: `${API_BASE}/mini/client/products`,
+  CLIENT_PRODUCT_CATEGORIES: `${API_BASE}/mini/client/product-categories`,
+  CLIENT_PRICE_LIST: `${API_BASE}/mini/client/price-list`,
+  CLIENT_PRICE_LIST_PRODUCTS: `${API_BASE}/mini/client/price-list/products`,
+
+  // 购物车
+  CART: `${API_BASE}/mini/client/cart`,
+  CART_BATCH: `${API_BASE}/mini/client/cart/batch`,
+
+  // 订单
+  ORDERS: `${API_BASE}/mini/client/orders`,
+  ORDER_PREVIEW: `${API_BASE}/mini/client/orders/preview`,
+  ORDER_FROM_CART: `${API_BASE}/mini/client/orders/from-cart`,
+  ORDER_DELIVERY_PROGRESS: `${API_BASE}/mini/client/orders/delivery-progress`,
+
+  // 供应商询价报价
+  SUPPLIER_INQUIRIES: `${API_BASE}/mini/supplier/inquiries`,
+  SUPPLIER_QUOTATIONS: `${API_BASE}/mini/supplier/quotations`,
+  SUPPLIER_PRICE_LISTS: `${API_BASE}/mini/supplier/price-lists`,
+  SUPPLIER_PICKUP_TASKS: `${API_BASE}/mini/supplier/pickup-tasks`,
+  SUPPLIER_TASKS: `${API_BASE}/mini/supplier/tasks`,
+
+  // 配送员任务
+  DELIVERER_TASKS: `${API_BASE}/mini/deliverer/tasks`,
+  DELIVERER_TASKS_STATISTICS: `${API_BASE}/mini/deliverer/tasks/statistics`,
+  DELIVERER_SNATCH: `${API_BASE}/mini/deliverer/snatch`,
+  DELIVERER_SNATCH_PENDING: `${API_BASE}/mini/deliverer/snatch/pending`,
+  DELIVERER_WARNINGS: `${API_BASE}/mini/deliverer/tasks/warnings`,
+  DELIVERER_TASKS_EXPIRING: `${API_BASE}/mini/deliverer/tasks/expiring`,
+
+  // 发票
+  CLIENT_INVOICES: `${API_BASE}/mini/client/invoices`,
+  INVOICE_AVAILABLE_ORDERS: `${API_BASE}/mini/client/invoices/available-orders`,
+
+  // 结算退款
+  CLIENT_SETTLEMENTS: `${API_BASE}/mini/client/settlements`,
+  CLIENT_SETTLEMENT_STATS: `${API_BASE}/mini/client/settlements/statistics`,
+  CLIENT_REFUNDS: `${API_BASE}/mini/client/refunds`,
+
+  // 消息(小程序专用路由)
+  MESSAGES: `${API_BASE}/mini/message/list`,
+  MESSAGE_UNREAD_COUNT: `${API_BASE}/mini/message/unread-count`,
+
+  // 公共
+  COMMON_SUPPLIERS: `${API_BASE}/mini/common/suppliers`,
+  CHINA_REGION_TREE: `${API_BASE}/mini/common/china-region-tree`,
+
+  // 统计
+  SUPPLIER_DASHBOARD: `${API_BASE}/mini/supplier/dashboard/statistics`,
+  CLIENT_DASHBOARD: `${API_BASE}/mini/client/dashboard/statistics`,
+
+  // 售后评价
+  PRODUCT_REVIEWS: `${API_BASE}/mini/reviews/product`,
+  CREATE_REVIEW: `${API_BASE}/mini/reviews`,
+  REVIEW_LIKE: (reviewId: string) => `${API_BASE}/mini/reviews/${reviewId}/like`,
+  REVIEW_STATISTICS: (productId: string) => `${API_BASE}/mini/reviews/statistics/${productId}`,
+}
+
+// Token 存储 Key
+export const TOKEN_KEY = 'sq_token'
+export const REFRESH_TOKEN_KEY = 'sq_refresh_token'
+export const USER_INFO_KEY = 'sq_user_info'

+ 89 - 0
.history/miniprogram/src/manifest_20260423152855.json

@@ -0,0 +1,89 @@
+{
+  "name": "陇耘鲜达",
+  "appid": "__UNI__SANQIANG",
+  "description": "陇耘鲜达小程序端",
+  "versionName": "1.0.0",
+  "versionCode": "100",
+  "transformPx": false,
+  "app-plus": {
+    "usingComponents": true,
+    "nvueStyleCompiler": "uni-app",
+    "compilerVersion": 3,
+    "splashscreen": {
+      "alwaysShowBeforeRender": true,
+      "waiting": true,
+      "autoclose": true,
+      "delay": 0
+    },
+    "modules": {},
+    "distribute": {
+      "android": {
+        "permissions": [
+          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+          "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+          "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+          "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+          "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+          "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+          "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+          "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+          "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+        ]
+      },
+      "ios": {},
+      "sdkConfigs": {}
+    }
+  },
+  "quickapp": {},
+  "mp-weixin": {
+    "appid": "wx1234567890abcdef",
+    "setting": {
+      "urlCheck": false,
+      "es6": true,
+      "postcss": true,
+      "minified": true
+    },
+    "usingComponents": true,
+    "permission": {
+      "scope.userLocation": {
+        "desc": "您的位置信息将用于小程序定位"
+      }
+    },
+    "requiredPrivateInfos": ["chooseAddress", "getLocation"]
+  },
+  "mp-alipay": {
+    "usingComponents": true
+  },
+  "mp-baidu": {
+    "usingComponents": true
+  },
+  "mp-toutiao": {
+    "usingComponents": true
+  },
+  "uniStatistics": {
+    "enable": false
+  },
+  "h5": {
+    "devServer": {
+      "port": 8080,
+      "disableHostCheck": true,
+      "proxy": {
+        "/api": {
+          "target": "http://192.168.2.110:8000",
+          "changeOrigin": true,
+          "secure": false
+        }
+      }
+    },
+    "router": {
+      "mode": "hash",
+      "base": "./"
+    },
+    "title": "陇耘鲜达"
+  }
+}

+ 2 - 2
.history/sqxp-wx/.env_20260422212056.development

@@ -1,8 +1,8 @@
 # sqxp-wx 开发/测试环境
 VITE_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
 VUE_APP_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
-VITE_API_BASE_URL=https://www.chengyantek.cn/sqxp-api/
-VUE_APP_API_BASE_URL=https://www.chengyantek.cn/sqxp-api/
+VITE_API_BASE_URL=http://192.168.2.110:8000/api
+VUE_APP_API_BASE_URL=http://192.168.2.110:8000/api
 
 # API 应用层加密
 VITE_ENABLE_API_CRYPTO=false

+ 2 - 2
.history/sqxp-wx/.env_20260422212120.development

@@ -1,8 +1,8 @@
 # sqxp-wx 开发/测试环境
 VITE_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
 VUE_APP_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
-VITE_API_BASE_URL=https://www.chengyantek.cn/sqxp-api/
-VUE_APP_API_BASE_URL=https://www.chengyantek.cn/sqxp-api/
+VITE_API_BASE_URL=http://192.168.2.110:8000/api
+VUE_APP_API_BASE_URL=http://192.168.2.110:8000/api
 
 # API 应用层加密
 VITE_ENABLE_API_CRYPTO=false

+ 9 - 0
.history/sqxp-wx/.env_20260423140727.development

@@ -0,0 +1,9 @@
+# sqxp-wx 开发/测试环境
+VITE_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VUE_APP_STATIC_ASSETS_BASE_URL=https://www.chengyantek.cn/sqxp-static/
+VITE_API_BASE_URL=http://192.168.2.110:8000/api
+VUE_APP_API_BASE_URL=http://192.168.2.110:8000/api
+
+# API 应用层加密
+VITE_ENABLE_API_CRYPTO=false
+VUE_APP_ENABLE_API_CRYPTO=false

+ 36 - 0
.history/sqxp-wx/config/api_20260423140939.js

@@ -0,0 +1,36 @@
+// API 基础配置
+const DEV_BASE_URL = 'http://192.168.2.110:8000/api'
+let devBaseUrl = DEV_BASE_URL
+
+// 微信小程序真机调试时,localhost 指向手机本机,需使用电脑局域网 IP。
+// #ifdef MP-WEIXIN
+devBaseUrl = 'http://192.168.2.110:8000/api'
+// #endif
+
+export const BASE_URL = process.env.NODE_ENV === 'development'
+  ? devBaseUrl
+  : 'http://192.168.2.110:8000/api'
+
+// 开发环境不需要 API 前缀 /api
+const API_PREFIX = process.env.NODE_ENV === 'development' ? '/' : ''
+
+export const API_BASE = `${BASE_URL}${API_PREFIX}`
+console.log('[ENV]', process.env.NODE_ENV, BASE_URL, API_BASE)
+
+/** 与 miniprogram `config/api.ts` 中客户/评价相关路径一致,供多页与 `api/reviewApi.js` 使用 */
+export const API_URL = {
+  CLIENT_DASHBOARD: `${API_BASE}/mini/client/dashboard/statistics`,
+  CLIENT_INVOICES: `${API_BASE}/mini/client/invoices`,
+  INVOICE_AVAILABLE_ORDERS: `${API_BASE}/mini/client/invoices/available-orders`,
+  CLIENT_SETTLEMENTS: `${API_BASE}/mini/client/settlements`,
+  CLIENT_SETTLEMENT_STATS: `${API_BASE}/mini/client/settlements/statistics`,
+  CLIENT_REFUNDS: `${API_BASE}/mini/client/refunds`,
+  PRODUCT_REVIEWS: `${API_BASE}/mini/reviews/product`,
+  CREATE_REVIEW: `${API_BASE}/mini/reviews`,
+  REVIEW_LIKE: (reviewId) => `${API_BASE}/mini/reviews/${reviewId}/like`,
+  REVIEW_STATISTICS: (productId) => `${API_BASE}/mini/reviews/statistics/${productId}`
+}
+
+export const TOKEN_KEY = 'sq_token'
+export const REFRESH_TOKEN_KEY = 'sq_refresh_token'
+export const USER_INFO_KEY = 'sq_user_info'

+ 36 - 0
.history/sqxp-wx/config/api_20260423141341.js

@@ -0,0 +1,36 @@
+// API 基础配置
+const DEV_BASE_URL = 'http://192.168.2.110:8000/api'
+let devBaseUrl = DEV_BASE_URL
+
+// 微信小程序真机调试时,localhost 指向手机本机,需使用电脑局域网 IP。
+// #ifdef MP-WEIXIN
+devBaseUrl = 'http://192.168.2.110:8000/api'
+// #endif
+
+export const BASE_URL = process.env.NODE_ENV === 'development'
+  ? devBaseUrl
+  : 'http://192.168.2.110:8000/api'
+
+// 开发环境不需要 API 前缀 /api
+const API_PREFIX = process.env.NODE_ENV === 'development' ? '' : ''
+
+export const API_BASE = `${BASE_URL}${API_PREFIX}`
+console.log('[ENV]', process.env.NODE_ENV, BASE_URL, API_BASE)
+
+/** 与 miniprogram `config/api.ts` 中客户/评价相关路径一致,供多页与 `api/reviewApi.js` 使用 */
+export const API_URL = {
+  CLIENT_DASHBOARD: `${API_BASE}/mini/client/dashboard/statistics`,
+  CLIENT_INVOICES: `${API_BASE}/mini/client/invoices`,
+  INVOICE_AVAILABLE_ORDERS: `${API_BASE}/mini/client/invoices/available-orders`,
+  CLIENT_SETTLEMENTS: `${API_BASE}/mini/client/settlements`,
+  CLIENT_SETTLEMENT_STATS: `${API_BASE}/mini/client/settlements/statistics`,
+  CLIENT_REFUNDS: `${API_BASE}/mini/client/refunds`,
+  PRODUCT_REVIEWS: `${API_BASE}/mini/reviews/product`,
+  CREATE_REVIEW: `${API_BASE}/mini/reviews`,
+  REVIEW_LIKE: (reviewId) => `${API_BASE}/mini/reviews/${reviewId}/like`,
+  REVIEW_STATISTICS: (productId) => `${API_BASE}/mini/reviews/statistics/${productId}`
+}
+
+export const TOKEN_KEY = 'sq_token'
+export const REFRESH_TOKEN_KEY = 'sq_refresh_token'
+export const USER_INFO_KEY = 'sq_user_info'

+ 168 - 0
.history/web-admin/vite.config_20260423140939.ts

@@ -0,0 +1,168 @@
+import { defineConfig, loadEnv } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import path from 'path'
+import { fileURLToPath } from 'url'
+import vueDevTools from 'vite-plugin-vue-devtools'
+import viteCompression from 'vite-plugin-compression'
+import Components from 'unplugin-vue-components/vite'
+import AutoImport from 'unplugin-auto-import/vite'
+import ElementPlus from 'unplugin-element-plus/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+import tailwindcss from '@tailwindcss/vite'
+// import { visualizer } from 'rollup-plugin-visualizer'
+
+export default ({ mode }: { mode: string }) => {
+  const root = process.cwd()
+  const env = loadEnv(mode, root)
+  const { VITE_VERSION, VITE_PORT, VITE_BASE_URL, VITE_API_URL, VITE_API_PROXY_URL } = env
+
+  console.log(`🚀 API_URL = ${VITE_API_URL}`)
+  console.log(`🚀 VERSION = ${VITE_VERSION}`)
+
+  return defineConfig({
+    define: {
+      __APP_VERSION__: JSON.stringify(VITE_VERSION)
+    },
+    base: VITE_BASE_URL,
+    server: {
+      port: Number(VITE_PORT),
+      proxy: {
+        '/api': {
+          target: VITE_API_PROXY_URL,
+          changeOrigin: true
+        },
+        // Web端接口代理 - 转发到后端 /api 前缀
+        '/web': {
+          target: 'http://192.168.2.110:8000/api',
+          changeOrigin: true,
+          rewrite: (path) => `/api${path}`
+        },
+        // 小程序端接口代理
+        '/mini': {
+          target: 'http://192.168.2.110:8000/api',
+          changeOrigin: true,
+          rewrite: (path) => `/api${path}`
+        }
+      },
+      host: true
+    },
+    // 路径别名
+    resolve: {
+      alias: {
+        '@': fileURLToPath(new URL('./src', import.meta.url)),
+        '@views': resolvePath('src/views'),
+        '@imgs': resolvePath('src/assets/images'),
+        '@icons': resolvePath('src/assets/icons'),
+        '@utils': resolvePath('src/utils'),
+        '@stores': resolvePath('src/store'),
+        '@styles': resolvePath('src/assets/styles')
+      }
+    },
+    build: {
+      target: 'es2015',
+      outDir: 'dist',
+      chunkSizeWarningLimit: 2000,
+      minify: 'terser',
+      terserOptions: {
+        compress: {
+          // 生产环境去除 console
+          drop_console: true,
+          // 生产环境去除 debugger
+          drop_debugger: true
+        }
+      },
+      dynamicImportVarsOptions: {
+        warnOnError: true,
+        exclude: [],
+        include: ['src/views/**/*.vue']
+      }
+    },
+    plugins: [
+      vue(),
+      tailwindcss(),
+      // 自动按需导入 API
+      AutoImport({
+        imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
+        dts: 'src/types/import/auto-imports.d.ts',
+        resolvers: [ElementPlusResolver()],
+        eslintrc: {
+          enabled: true,
+          filepath: './.auto-import.json',
+          globalsPropValue: true
+        }
+      }),
+      // 自动按需导入组件
+      Components({
+        dts: 'src/types/import/components.d.ts',
+        resolvers: [ElementPlusResolver()]
+      }),
+      // 按需定制主题配置
+      ElementPlus({
+        useSource: true
+      }),
+      // 压缩
+      viteCompression({
+        verbose: false, // 是否在控制台输出压缩结果
+        disable: false, // 是否禁用
+        algorithm: 'gzip', // 压缩算法
+        ext: '.gz', // 压缩后的文件名后缀
+        threshold: 10240, // 只有大小大于该值的资源会被处理 10240B = 10KB
+        deleteOriginFile: false // 压缩后是否删除原文件
+      }),
+      vueDevTools()
+      // 打包分析
+      // visualizer({
+      //   open: true,
+      //   gzipSize: true,
+      //   brotliSize: true,
+      //   filename: 'dist/stats.html' // 分析图生成的文件名及路径
+      // }),
+    ],
+    // 依赖预构建:避免运行时重复请求与转换,提升首次加载速度
+    optimizeDeps: {
+      include: [
+        'echarts/core',
+        'echarts/charts',
+        'echarts/components',
+        'echarts/renderers',
+        'xlsx',
+        'xgplayer',
+        'crypto-js',
+        'file-saver',
+        'vue-img-cutter',
+        'element-plus/es',
+        'element-plus/es/components/*/style/css',
+        'element-plus/es/components/*/style/index'
+      ]
+    },
+    css: {
+      preprocessorOptions: {
+        // sass variable and mixin
+        scss: {
+          additionalData: `
+            @use "@styles/core/el-light.scss" as *; 
+            @use "@styles/core/mixin.scss" as *;
+          `
+        }
+      },
+      postcss: {
+        plugins: [
+          {
+            postcssPlugin: 'internal:charset-removal',
+            AtRule: {
+              charset: (atRule) => {
+                if (atRule.name === 'charset') {
+                  atRule.remove()
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  })
+}
+
+function resolvePath(paths: string) {
+  return path.resolve(__dirname, paths)
+}

+ 78 - 0
backend/app/api/mini_client_extra.py

@@ -735,6 +735,84 @@ def get_client_product_detail(
     })
 
 
+@router.get("/products-ro/{product_id}", response_model=dict)
+def get_client_product_detail_readonly(
+    product_id: str,
+    db: Session = Depends(get_db),
+    current_user: dict = Depends(require_client),
+):
+    """客户端只读商品详情:用于订单历史等场景,不依赖区域价目表。"""
+    product = product_crud.get(db, product_id)
+    if not product or getattr(product, "is_del", 0) == 1:
+        return error_response(404, "商品不存在")
+
+    unit = unit_crud.get(db, product.unit_id) if getattr(product, "unit_id", None) else None
+    cat_name = ""
+    if getattr(product, "category_id", None):
+        cat = category_crud.get(db, product.category_id)
+        cat_name = cat.name if cat else ""
+
+    banners_raw = product_banner.get_by_product(db, product_id)
+    banners = [{
+        "mediaType": b.media_type,
+        "fileUrl": b.file_url,
+        "thumbnailUrl": b.thumbnail_url,
+        "sortOrder": b.sort_order,
+    } for b in banners_raw]
+
+    pv_rows = product_parameter_value.get_by_product_with_detail(db, product_id)
+    parameter_values = []
+    for row in pv_rows:
+        val = row.get("param_value")
+        if val is None or str(val).strip() == "":
+            continue
+        parameter_values.append({
+            "parameterId": row.get("parameter_id"),
+            "parameterName": row.get("parameter_name") or "",
+            "paramValue": str(val),
+        })
+
+    media_raw = product_detail_media.get_by_product(db, product_id)
+    detail_media = []
+    for m in media_raw:
+        if m.content_type not in ("image", "video"):
+            continue
+        if not m.file_url:
+            continue
+        detail_media.append({
+            "id": m.id,
+            "contentType": m.content_type,
+            "fileUrl": m.file_url,
+            "thumbnailUrl": m.thumbnail_url,
+            "description": m.description,
+            "sortOrder": m.sort_order,
+        })
+
+    product_payload = {
+        "id": product.id,
+        "name": product.name,
+        "skuCode": product.sku_code,
+        "categoryId": product.category_id,
+        "categoryName": cat_name,
+        "unitId": getattr(product, "unit_id", None),
+        "unitName": unit.name if unit else None,
+        "measureValue": float(product.measure_value) if getattr(product, "measure_value", None) is not None else None,
+        # 价格由前端在只读订单场景使用历史成交价(fallbackPrice)展示,这里不做价目表绑定
+        "price": 0,
+        "imageUrl": product.image_url,
+        "description": product.description or "",
+        "detailContent": product.detail_content or "",
+        "isActive": bool(getattr(product, "is_active", 1)),
+    }
+
+    return success_response({
+        "product": product_payload,
+        "banners": banners,
+        "parameterValues": parameter_values,
+        "detailMedia": detail_media,
+    })
+
+
 @router.post("/orders/from-cart", response_model=dict)
 def create_order_from_cart(
     request: dict,

+ 1 - 1
miniprogram/README.md

@@ -99,7 +99,7 @@ npm run build:h5
 ## API 配置
 
 修改 `src/config/api.ts` 中的 `BASE_URL` 为实际的接口地址:
-- 开发环境:`https://www.chengyantek.cn/sqxp-api`
+- 开发环境:`http://192.168.2.110:8000/api`
 - 生产环境:`https://api.sanqiang.com`
 
 ## 注意事项

+ 1 - 1
miniprogram/src/config/api.ts

@@ -4,7 +4,7 @@
 
 // API 基础地址
 export const BASE_URL = process.env.NODE_ENV === 'development'
-  ? 'http://192.168.2.105:8000'
+  ? 'http://192.168.2.110:8000'
   : 'https://api.sanqiang.com'
 
 // API 前缀

+ 1 - 1
miniprogram/src/manifest.json

@@ -74,7 +74,7 @@
       "disableHostCheck": true,
       "proxy": {
         "/api": {
-          "target": "http://192.168.2.105:8000",
+          "target": "http://192.168.2.110:8000",
           "changeOrigin": true,
           "secure": false
         }

+ 246 - 14
sqxp-wx/components/client/product-detail-overlay.vue

@@ -20,7 +20,7 @@
             <view class="skeleton-row skeleton-row--short shimmer" />
           </view>
         </view>
-        <view v-else-if="!product" class="empty">商品不存在</view>
+        <view v-else-if="!product && !hasSoldOutFallback" class="empty">商品不存在</view>
         <view v-else class="overlay-main">
           <view class="overlay-sticky-top">
             <view class="banner-wrap">
@@ -60,7 +60,7 @@
                 <text class="banner-empty-text">暂无轮播图</text>
               </view>
 
-              <view class="banner-close" :style="{ top: `${safeTop + 8}px` }" @click="emitClose">
+              <view class="banner-close" :style="{ '--sq-safe-top': `${safeTop}px` }" @click="emitClose">
                 <image class="banner-close-icon" :src="ICON_ARROW_BOTTOM" mode="aspectFit" />
               </view>
             </view>
@@ -68,11 +68,12 @@
             <view class="product-info">
               <view class="info-top-row">
                 <view class="info-left">
-                  <view v-if="!props.readonly" class="price-row">
-                    <text class="price">¥{{ formatPrice(product.price) }}</text>
-                    <text class="unit">/{{ formatMeasureText(product.measureValue, product.unitName) }}</text>
+                  <text class="name">{{ displayProductName }}</text>
+                  <text v-if="showSoldOutHint" class="sold-out-hint">{{ soldOutHintText }}</text>
+                  <view v-if="showPriceRow" class="price-row">
+                    <text class="price">¥{{ formatPrice(displayPriceFen) }}</text>
+                    <text v-if="showUnitInPriceRow" class="unit">/{{ displayMeasureText }}</text>
                   </view>
-                  <text class="name">{{ product.name }}</text>
                 </view>
 
                 <view v-if="!props.readonly" class="info-right" @click.stop>
@@ -95,6 +96,28 @@
 
           <scroll-view scroll-y class="overlay-scroll" :show-scrollbar="true">
             <view class="overlay-scroll-inner">
+              <view v-if="purchaseInfo" class="purchase-section">
+                <view class="section-title">采购信息</view>
+                <view class="purchase-grid">
+                  <view class="purchase-item">
+                    <text class="purchase-label">规格</text>
+                    <text class="purchase-value">{{ purchaseInfo.measureText || '-' }}</text>
+                  </view>
+                  <view class="purchase-item">
+                    <text class="purchase-label">单价</text>
+                    <text class="purchase-value">¥{{ formatPrice(purchaseInfo.unitPrice) }}</text>
+                  </view>
+                  <view class="purchase-item">
+                    <text class="purchase-label">数量</text>
+                    <text class="purchase-value">{{ purchaseInfo.quantity == null ? '-' : purchaseInfo.quantity }}</text>
+                  </view>
+                  <view class="purchase-item">
+                    <text class="purchase-label">小计</text>
+                    <text class="purchase-value purchase-value--strong">¥{{ formatPrice(purchaseInfo.amount) }}</text>
+                  </view>
+                </view>
+              </view>
+
               <view class="detail-section" v-if="detailContent">
                 <view class="section-title">商品详情</view>
                 <view class="rich-content">
@@ -188,7 +211,14 @@ const props = defineProps({
   readonly: { type: Boolean, default: false },
   bottomMode: { type: String, default: 'checkout' },
   tabBarActive: { type: String, default: undefined },
-  initialCartQty: { type: Number, default: undefined }
+  initialCartQty: { type: Number, default: undefined },
+  fallbackProductName: { type: String, default: '' },
+  fallbackMeasureText: { type: String, default: '' },
+  fallbackPrice: { type: Number, default: null },
+  fallbackImageUrl: { type: String, default: '' },
+  fallbackQuantity: { type: Number, default: null },
+  fallbackLineAmount: { type: Number, default: null },
+  fallbackHintText: { type: String, default: '此商品目前已售罄' }
 })
 
 const emit = defineEmits(['close'])
@@ -203,6 +233,7 @@ const detailContent = ref('')
 const detailMediaList = ref([])
 const bannerMediaLoadedMap = ref({})
 const detailMediaLoadedMap = ref({})
+const soldOutFallbackActive = ref(false)
 
 const cartCount = ref(0)
 const localCartQty = ref(null)
@@ -243,6 +274,53 @@ const currentCartQty = computed(() => {
   return 0
 })
 
+const hasSoldOutFallback = computed(() => {
+  if (!props.readonly || !props.fallbackProductName) return false
+  return soldOutFallbackActive.value || !props.productId
+})
+const soldOutHintText = computed(() => (props.fallbackHintText || '此商品目前已售罄').trim())
+const unavailableInRegion = ref(false)
+const displayProductName = computed(() => {
+  if (product.value?.name) return product.value.name
+  if (hasSoldOutFallback.value) return props.fallbackProductName
+  return ''
+})
+const displayMeasureText = computed(() => {
+  if (hasSoldOutFallback.value) return props.fallbackMeasureText || ''
+  if (product.value) return formatMeasureText(product.value.measureValue, product.value.unitName)
+  return ''
+})
+const displayPriceFen = computed(() => {
+  // 订单详情只读场景:优先展示历史成交价(fallbackPrice),不受当前价目表影响
+  if (props.readonly && props.fallbackPrice != null) return Number(props.fallbackPrice) || 0
+  if (product.value?.price != null) return Number(product.value.price) || 0
+  if (hasSoldOutFallback.value && props.fallbackPrice != null) return Number(props.fallbackPrice) || 0
+  return null
+})
+const showPriceRow = computed(() => displayPriceFen.value != null)
+const showUnitInPriceRow = computed(() => {
+  if (!displayMeasureText.value) return false
+  // 订单详情等只读弹窗:价格后计量单位需要展示(即使当前区域不出售)
+  if (props.readonly) return true
+  return !unavailableInRegion.value
+})
+const showSoldOutHint = computed(() => hasSoldOutFallback.value || unavailableInRegion.value)
+
+const purchaseInfo = computed(() => {
+  // 仅在无法加载商品详情、降级到历史快照时展示“采购信息”
+  if (!props.readonly || !soldOutFallbackActive.value) return null
+  const qty = props.fallbackQuantity
+  const unitPrice = props.fallbackPrice
+  const amount = props.fallbackLineAmount
+  const measureText = (props.fallbackMeasureText || '').trim()
+  return {
+    unitPrice: unitPrice == null ? null : Number(unitPrice) || 0,
+    quantity: qty == null ? null : Number(qty) || 0,
+    amount: amount == null ? null : Number(amount) || 0,
+    measureText
+  }
+})
+
 function resetMediaLoadingState() {
   bannerMediaLoadedMap.value = {}
   detailMediaLoadedMap.value = {}
@@ -331,18 +409,67 @@ async function updateQuantity(delta) {
 
 async function loadProductDetail(productId) {
   loading.value = true
+  soldOutFallbackActive.value = false
+  unavailableInRegion.value = false
   resetMediaLoadingState()
+  const applySoldOutFallback = () => {
+    soldOutFallbackActive.value = true
+    product.value = {
+      id: productId || '',
+      name: props.fallbackProductName || '',
+      measureValue: '',
+      unitName: '',
+      price: props.fallbackPrice == null ? 0 : Number(props.fallbackPrice) || 0
+    }
+    bannerList.value = props.fallbackImageUrl
+      ? [{ mediaType: 'image', fileUrl: props.fallbackImageUrl }]
+      : []
+    parameterValues.value = []
+    detailContent.value = ''
+    detailMediaList.value = []
+    bannerMediaLoadedMap.value = Object.fromEntries(bannerList.value.map((_, i) => [i, false]))
+    detailMediaLoadedMap.value = {}
+  }
   try {
-    const res = await request({
-      url: `/mini/client/products/${productId}`,
-      method: 'GET'
-    })
-    const body = parseResponseJson(res) || {}
+    // 只读(订单详情)优先尝试价目表接口:用于判断“该区域是否出售”
+    let body = null
+    if (props.readonly) {
+      const resCheck = await request({ url: `/mini/client/products/${productId}`, method: 'GET' })
+      const checkBody = parseResponseJson(resCheck) || {}
+      const msg = String(checkBody.message || '')
+      const notInPriceList = msg.includes('商品不存在或不在当前价目表')
+      if (checkBody.code === 200 && checkBody.data) {
+        body = checkBody
+      } else if (notInPriceList) {
+        unavailableInRegion.value = true
+      }
+    }
+
+    // 非只读 / 或只读但价目表接口不可用:拉“只读详情”保证内容完整展示
+    if (!body) {
+      const res = await request({
+        url: props.readonly ? `/mini/client/products-ro/${productId}` : `/mini/client/products/${productId}`,
+        method: 'GET'
+      })
+      body = parseResponseJson(res) || {}
+    }
+
     if (body.code !== 200 || !body.data) {
       product.value = null
+      const msg = String(body.message || '')
+      const unavailable = msg.includes('商品不存在或不在当前价目表')
+      if (props.readonly && unavailable && props.fallbackProductName) {
+        applySoldOutFallback()
+        return
+      }
+      if (props.readonly && props.fallbackProductName) {
+        applySoldOutFallback()
+        return
+      }
       uni.showToast({ title: body.message || '加载失败', icon: 'none' })
       return
     }
+
     product.value = body.data.product || body.data || null
     bannerList.value = Array.isArray(body.data.banners) ? body.data.banners : []
     parameterValues.value = Array.isArray(body.data.parameterValues) ? body.data.parameterValues : []
@@ -360,6 +487,12 @@ async function loadProductDetail(productId) {
 
     bannerMediaLoadedMap.value = Object.fromEntries(bannerList.value.map((_, i) => [i, false]))
     detailMediaLoadedMap.value = Object.fromEntries(detailMediaList.value.map((_, i) => [i, false]))
+  } catch {
+    if (props.readonly && props.fallbackProductName) {
+      applySoldOutFallback()
+      return
+    }
+    uni.showToast({ title: '加载失败', icon: 'none' })
   } finally {
     loading.value = false
   }
@@ -377,8 +510,23 @@ function goToCart() {
 
 function ensureSafeTop() {
   const si = uni.getSystemInfoSync?.()
-  const top = si?.safeAreaInsets?.top ?? 0
-  safeTop.value = top
+  let top = 0
+  // H5 真机(浏览器/微信内置浏览器)下 safeAreaInsets.top 在部分机型会偏大,
+  // 这里统一让 H5 走 CSS 的 env(safe-area-inset-top);小程序端再走系统值。
+  // #ifdef MP-WEIXIN
+  // 统一口径:让“关闭按钮”的顶部偏移 = 设备安全区顶部 - 当前页面 webview 顶部(windowTop)
+  // - 自定义导航栏页:windowTop 通常为 0 => top ≈ statusBarHeight
+  // - 原生导航栏页:windowTop 通常包含导航栏占位 => top 变为 0(避免叠加导致按钮偏下)
+  const safeInsetTop = Number(si?.safeAreaInsets?.top ?? si?.statusBarHeight ?? 0) || 0
+  const windowTop = Number(si?.windowTop ?? 0) || 0
+  top = safeInsetTop - windowTop
+  // #endif
+  // #ifndef MP-WEIXIN
+  top = 0
+  // #endif
+  // 保护:出现异常大值时,直接归零(避免按钮被推得很靠下)
+  const n = Math.max(0, Number(top) || 0)
+  safeTop.value = n > 60 ? 0 : n
 }
 
 watch(
@@ -393,6 +541,7 @@ watch(
       loading.value = false
       localCartQty.value = null
       qtyUpdating.value = false
+      soldOutFallbackActive.value = false
     }
   },
   { immediate: true }
@@ -593,6 +742,7 @@ watch(
 .banner-media-wrap,
 .detail-media-wrap {
   position: relative;
+  height: 100%;
 }
 
 .banner-empty {
@@ -610,6 +760,14 @@ watch(
   }
 }
 
+/* 小程序端对 aspect-ratio 支持不稳定:给轮播图明确高度,避免 loading 遮罩飘到顶部 */
+/* #ifndef H5 */
+.banner-swiper,
+.banner-empty {
+  height: 540rpx;
+}
+/* #endif */
+
 .banner-close {
   position: absolute;
   left: 20rpx;
@@ -624,6 +782,20 @@ watch(
   border: 1rpx solid rgba(221, 229, 235, 0.9);
 }
 
+/* 小程序:只用系统状态栏高度(safeTop),避免与 env(...) 叠加导致 top 过大 */
+/* #ifdef MP-WEIXIN */
+.banner-close {
+  top: calc(var(--sq-safe-top, 0px) + 8px);
+}
+/* #endif */
+
+/* H5:用 env(safe-area-inset-top) 适配刘海屏 */
+/* #ifndef MP-WEIXIN */
+.banner-close {
+  top: calc(env(safe-area-inset-top, 0px) + 8px);
+}
+/* #endif */
+
 .banner-close-icon {
   width: 28rpx;
   height: 28rpx;
@@ -684,6 +856,22 @@ watch(
     word-break: break-word;
     overflow-wrap: break-word;
   }
+
+  .measure-text {
+    display: block;
+    margin-top: 10rpx;
+    font-size: 24rpx;
+    color: var(--c-muted);
+    line-height: 1.4;
+  }
+
+  .sold-out-hint {
+    display: block;
+    margin-top: 8rpx;
+    font-size: 24rpx;
+    color: #d97706;
+    line-height: 1.4;
+  }
 }
 
 .add-btn {
@@ -847,6 +1035,50 @@ watch(
   }
 }
 
+.purchase-section {
+  margin-top: 0;
+  background: #fff;
+  border-bottom: 12rpx solid var(--c-bg);
+}
+
+.purchase-grid {
+  padding: 6rpx 20rpx 18rpx;
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16rpx 18rpx;
+  box-sizing: border-box;
+}
+
+.purchase-item {
+  min-width: 0;
+  padding: 14rpx 16rpx;
+  border: 1rpx solid #eef3f0;
+  border-radius: 18rpx;
+  background: #fbfdfc;
+  box-sizing: border-box;
+}
+
+.purchase-label {
+  display: block;
+  font-size: 22rpx;
+  color: var(--c-muted);
+  line-height: 1.2;
+}
+
+.purchase-value {
+  display: block;
+  margin-top: 10rpx;
+  font-size: 28rpx;
+  color: var(--c-text);
+  font-weight: 600;
+  line-height: 1.25;
+  word-break: break-word;
+}
+
+.purchase-value--strong {
+  color: var(--c-danger);
+}
+
 /* 最后一个内容块不再额外留底部分隔,避免尾部出现空白条 */
 .overlay-scroll-inner > view:last-child {
   border-bottom: 0;

+ 4 - 4
sqxp-wx/config/api.js

@@ -1,18 +1,18 @@
 // API 基础配置
-const DEV_BASE_URL = 'https://www.chengyantek.cn/sqxp-api'
+const DEV_BASE_URL = 'http://192.168.2.110:8000/api'
 let devBaseUrl = DEV_BASE_URL
 
 // 微信小程序真机调试时,localhost 指向手机本机,需使用电脑局域网 IP。
 // #ifdef MP-WEIXIN
-devBaseUrl = 'https://www.chengyantek.cn/sqxp-api'
+devBaseUrl = 'http://192.168.2.110:8000/api'
 // #endif
 
 export const BASE_URL = process.env.NODE_ENV === 'development'
   ? devBaseUrl
-  : 'https://www.chengyantek.cn/sqxp-api'
+  : 'http://192.168.2.110:8000/api'
 
 // 开发环境不需要 API 前缀 /api
-const API_PREFIX = process.env.NODE_ENV === 'development' ? '/' : ''
+const API_PREFIX = process.env.NODE_ENV === 'development' ? '' : ''
 
 export const API_BASE = `${BASE_URL}${API_PREFIX}`
 console.log('[ENV]', process.env.NODE_ENV, BASE_URL, API_BASE)

+ 1 - 0
sqxp-wx/pages.json

@@ -40,6 +40,7 @@
 		{
 			"path": "pages/client/order/detail/detail",
 			"style": {
+				"navigationStyle": "custom",
 				"navigationBarTitleText": "订单详情",
 				"navigationBarBackgroundColor": "#f3f7f5",
 				"navigationBarTextStyle": "black",

+ 13 - 4
sqxp-wx/pages/client/address/address.vue

@@ -135,6 +135,13 @@ const deletingItem = ref(null)
 
 const PAGE_SIZE = 20
 
+function prioritizeDefaultAddress(items = []) {
+  if (!Array.isArray(items) || items.length <= 1) return items
+  const sorted = [...items]
+  sorted.sort((a, b) => Number(!!b?.isDefault) - Number(!!a?.isDefault))
+  return sorted
+}
+
 /** reset=true:从第一页重新拉(onShow、删改后);false:滚动加载下一页 */
 async function fetchList(reset = true) {
   if (reset) {
@@ -161,15 +168,17 @@ async function fetchList(reset = true) {
       const items = Array.isArray(d.list) ? d.list : []
       const totalPages = Number(d.totalPages ?? 0)
       if (reset) {
-        list.value = items
+        list.value = prioritizeDefaultAddress(items)
       } else {
-        const seen = new Set(list.value.map((x) => x.id))
-        for (const it of items) {
+        const merged = [...list.value]
+        const seen = new Set(merged.map((x) => x.id))
+        for (const it of prioritizeDefaultAddress(items)) {
           if (it.id && !seen.has(it.id)) {
-            list.value.push(it)
+            merged.push(it)
             seen.add(it.id)
           }
         }
+        list.value = prioritizeDefaultAddress(merged)
       }
       hasMore.value = totalPages > 0 ? page < totalPages : items.length >= PAGE_SIZE
       nextPage.value = page + 1

+ 2 - 2
sqxp-wx/pages/client/address/edit.vue

@@ -343,7 +343,7 @@ onLoad(async (options) => {
   border-radius: var(--c-radius-card);
   border: 1rpx solid var(--c-border);
   padding: 8rpx 24rpx 40rpx;
-  margin: 0;
+  margin: 24rpx 0 0;
   box-sizing: border-box;
 }
 
@@ -450,7 +450,7 @@ onLoad(async (options) => {
   height: 88rpx;
   line-height: 88rpx;
   margin-top: 40rpx;
-  background: linear-gradient(145deg, var(--c-barley-yellow), #e4b458);
+  background: linear-gradient(145deg, var(--c-brand-green), var(--c-brand-green-2));
   color: #ffffff;
   border-radius: 44rpx;
   font-size: 30rpx;

+ 171 - 26
sqxp-wx/pages/client/cart/cart.vue

@@ -2,7 +2,7 @@
   <view class="cart-root client-theme">
   <view class="cart-page">
     <!-- 对齐 sanqiang-client-cart-ui-demo.html .top -->
-    <view class="cart-hero sq-hero-header sq-hero-header--compact">
+    <view class="cart-hero sq-hero-header sq-hero-header--compact sq-hero-header--client">
       <text class="cart-hero-title">购物车</text>
       <text class="cart-hero-sub">支持一键结算</text>
     </view>
@@ -40,7 +40,10 @@
       <view v-else-if="!cartStore.isEmpty" class="cart-items-inner">
         <view v-for="g in groupedCartLines" :key="g.key" class="category-group">
           <view class="category-header">
-            <text class="category-name">{{ g.title }}</text>
+            <view class="category-title-wrap">
+              <image class="category-icon" :src="g.imageUrl" mode="aspectFill" />
+              <text class="category-name">{{ g.title }}</text>
+            </view>
           </view>
 
           <view v-for="item in g.items" :key="item.id" class="cart-item" hover-class="cart-item--pressed" :hover-stay-time="100">
@@ -117,6 +120,32 @@
       </view>
     </view>
 
+    <view
+      v-if="showDeleteDialog"
+      class="delete-dialog-mask"
+      @click="closeDeleteDialog"
+    >
+      <view class="delete-dialog" @click.stop>
+        <view class="delete-dialog__icon">!</view>
+        <text class="delete-dialog__title">删除商品</text>
+        <text class="delete-dialog__desc">确认将该商品从购物车移除吗?</text>
+        <view class="delete-dialog__actions">
+          <view
+            class="delete-dialog__action delete-dialog__action--cancel"
+            @click="closeDeleteDialog"
+          >
+            取消
+          </view>
+          <button
+            class="delete-dialog__action delete-dialog__action--confirm"
+            @click="confirmRemoveItem"
+          >
+            删除
+          </button>
+        </view>
+      </view>
+    </view>
+
     <ProductDetailOverlay
       :show="showProductOverlay"
       :product-id="overlayProductId"
@@ -166,6 +195,8 @@ const selectedAddress = computed(
 
 const selectedIds = ref(new Set())
 const quantityUpdatingIds = ref(new Set())
+const showDeleteDialog = ref(false)
+const pendingDeleteCartId = ref('')
 const selectedCount = computed(() =>
   cartLines.value.reduce((sum, i) => sum + (selectedIds.value.has(i.id) ? Number(i.quantity) || 0 : 0), 0)
 )
@@ -191,6 +222,13 @@ const groupedCartLines = computed(() => {
       groups.push({
         key,
         title: it.firstCategoryName || it.categoryName || '未分类',
+        imageUrl:
+          it.firstCategoryImage ||
+          it.firstCategoryImageUrl ||
+          it.categoryImage ||
+          it.categoryIcon ||
+          it.imageUrl ||
+          ICON_CATEGORY_FALLBACK,
         items: [it]
       })
     }
@@ -200,6 +238,7 @@ const groupedCartLines = computed(() => {
 
 const ICON_COORDINATE = staticAssetUrl('coordinate.svg')
 const ICON_DELETE = staticAssetUrl('delete.svg')
+const ICON_CATEGORY_FALLBACK = staticAssetUrl('product-icon.svg')
 
 async function loadCartList(reset) {
   if (cartListLoading.value) return
@@ -392,27 +431,33 @@ async function updateQuantity(cartId, delta) {
 }
 
 // 删除商品
-async function removeItem(cartId) {
-  uni.showModal({
-    title: '提示',
-    content: '确定删除该商品?',
-    success: async (res) => {
-      if (res.confirm) {
-        const target = cartLines.value.find(i => i.id === cartId)
-        const removedIndex = cartLines.value.findIndex(i => i.id === cartId)
-        cartLines.value = cartLines.value.filter(i => i.id !== cartId)
-        const next = new Set(selectedIds.value)
-        next.delete(cartId)
-        selectedIds.value = next
-        const ok = await cartStore.removeItem(cartId)
-        if (!ok && target) {
-          const restored = [...cartLines.value]
-          restored.splice(Math.max(removedIndex, 0), 0, target)
-          cartLines.value = restored
-        }
-      }
-    }
-  })
+function removeItem(cartId) {
+  if (!cartId) return
+  pendingDeleteCartId.value = cartId
+  showDeleteDialog.value = true
+}
+
+function closeDeleteDialog() {
+  showDeleteDialog.value = false
+  pendingDeleteCartId.value = ''
+}
+
+async function confirmRemoveItem() {
+  const cartId = pendingDeleteCartId.value
+  if (!cartId) return
+  closeDeleteDialog()
+  const target = cartLines.value.find(i => i.id === cartId)
+  const removedIndex = cartLines.value.findIndex(i => i.id === cartId)
+  cartLines.value = cartLines.value.filter(i => i.id !== cartId)
+  const next = new Set(selectedIds.value)
+  next.delete(cartId)
+  selectedIds.value = next
+  const ok = await cartStore.removeItem(cartId)
+  if (!ok && target) {
+    const restored = [...cartLines.value]
+    restored.splice(Math.max(removedIndex, 0), 0, target)
+    cartLines.value = restored
+  }
 }
 
 // 清空购物车
@@ -703,6 +748,21 @@ page {
     padding: 20rpx 0 16rpx;
     border-bottom: 1rpx solid #edf5f1;
 
+    .category-title-wrap {
+      display: inline-flex;
+      align-items: center;
+      gap: 10rpx;
+      min-width: 0;
+    }
+
+    .category-icon {
+      width: 34rpx;
+      height: 34rpx;
+      border-radius: 8rpx;
+      flex: 0 0 auto;
+      background: #f3f7f5;
+    }
+
     .category-name {
       font-size: 28rpx;
       font-weight: 600;
@@ -962,12 +1022,11 @@ page {
 .checkout-bar {
   position: fixed;
   /* #ifdef H5 */
-  bottom: calc(var(--window-bottom, calc(50px + env(safe-area-inset-bottom, 0px))) + 44rpx);
+  bottom: calc(var(--window-bottom, calc(50px + env(safe-area-inset-bottom, 0px))) + 4rpx);
   z-index: 10010;
   /* #endif */
   /* #ifndef H5 */
-  bottom: calc(100rpx + constant(safe-area-inset-bottom));
-  bottom: calc(100rpx + env(safe-area-inset-bottom, 0px));
+  bottom: 10px;
   z-index: 10010;
   /* #endif */
   left: 50%;
@@ -981,8 +1040,10 @@ page {
   display: flex;
   align-items: center;
   justify-content: center;
+  /* #ifdef H5 */
   padding-bottom: calc(6rpx + constant(safe-area-inset-bottom));
   padding-bottom: calc(6rpx + env(safe-area-inset-bottom));
+  /* #endif */
   overflow: visible;
 }
 
@@ -1070,4 +1131,88 @@ page {
     color: #fff;
   }
 }
+
+.delete-dialog-mask {
+  position: fixed;
+  inset: 0;
+  z-index: 12000;
+  background: rgba(18, 28, 24, 0.45);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40rpx;
+  box-sizing: border-box;
+}
+
+.delete-dialog {
+  width: 100%;
+  max-width: 620rpx;
+  background: #ffffff;
+  border-radius: 26rpx;
+  padding: 40rpx 34rpx 32rpx;
+  box-sizing: border-box;
+  box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.15);
+}
+
+.delete-dialog__icon {
+  width: 76rpx;
+  height: 76rpx;
+  margin: 0 auto 20rpx;
+  border-radius: 50%;
+  background: #fff9ec;
+  color: #d97706;
+  font-size: 42rpx;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.delete-dialog__title {
+  display: block;
+  text-align: center;
+  color: var(--c-text);
+  font-size: 34rpx;
+  font-weight: 700;
+}
+
+.delete-dialog__desc {
+  display: block;
+  margin-top: 14rpx;
+  text-align: center;
+  color: var(--c-muted);
+  font-size: 26rpx;
+  line-height: 1.6;
+}
+
+.delete-dialog__actions {
+  margin-top: 28rpx;
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  column-gap: 16rpx;
+}
+
+.delete-dialog__action {
+  width: 100%;
+  min-height: 76rpx;
+  border-radius: 999rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  font-weight: 600;
+  border: none;
+  box-sizing: border-box;
+}
+
+.delete-dialog__action--cancel {
+  background: #f4f7f6;
+  color: #5b6b64;
+  border: 1rpx solid #ddebe5;
+}
+
+.delete-dialog__action--confirm {
+  background: linear-gradient(145deg, var(--c-barley-yellow), #e4b458);
+  color: #ffffff;
+}
 </style>

+ 21 - 6
sqxp-wx/pages/client/home/home.vue

@@ -7,10 +7,13 @@
     </view>
     <view v-else class="content-wrap">
       <!-- 顶栏:仅渐变背景 + 配送行 + 搜索条(与 sanqiang-client-ui-demo.html .top 一致) -->
-      <view class="filter-top-wrap sq-hero-header">
+      <view class="filter-top-wrap sq-hero-header sq-hero-header--client">
         <view class="hero-top-main">
           <view class="hero-top-text">
-            <text class="hero-brand">陇耘鲜达</text>
+            <view class="hero-brand-row">
+              <image class="hero-brand-logo" :src="IMG_LOGO_WATERMARK" mode="aspectFit" />
+              <text class="hero-brand">陇耘鲜达</text>
+            </view>
             <text class="hero-region-line">{{ deliveryLineLeft }}</text>
           </view>
           <ClientImageLoader
@@ -713,13 +716,26 @@ page {
   align-items: flex-start;
 }
 
+.hero-brand-row {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  margin-bottom: 12rpx;
+}
+
+.hero-brand-logo {
+  width: 88rpx;
+  height: 88rpx;
+  border-radius: 12rpx;
+  flex-shrink: 0;
+}
+
 .hero-brand {
   color: #ffffff;
   font-size: 48rpx;
   font-weight: 700;
   line-height: 1.25;
   letter-spacing: 0.5rpx;
-  margin-bottom: 12rpx;
 }
 
 .hero-region-line {
@@ -1173,11 +1189,10 @@ page {
   overflow: visible;
   /* 紧贴 tab 上沿:略留一点避免与底栏视觉粘连(H5 原 +44rpx 空隙偏大) */
   /* #ifdef H5 */
-  bottom: calc(var(--window-bottom, calc(50px + env(safe-area-inset-bottom, 0px))) + 10rpx);
+  bottom: calc(var(--window-bottom, calc(50px + env(safe-area-inset-bottom, 0px))) + 2rpx);
   /* #endif */
   /* #ifndef H5 */
-  bottom: calc(92rpx + constant(safe-area-inset-bottom));
-  bottom: calc(92rpx + env(safe-area-inset-bottom, 0px));
+  bottom: 10px;
   /* #endif */
 }
 

+ 27 - 1
sqxp-wx/pages/client/invoice/invoice.vue

@@ -35,7 +35,16 @@
           @change="onPickerChange"
         >
           <view class="picker">
-            订单:{{ availOrders[pickerIndex]?.orderNo || '选择订单' }}
+            <view class="picker-row">
+              <text class="picker-text">订单:{{ availOrders[pickerIndex]?.orderNo || '选择订单' }}</text>
+              <SqCopyBtn
+                v-if="availOrders[pickerIndex]?.orderNo"
+                class="picker-copy"
+                :text="availOrders[pickerIndex]?.orderNo"
+                label="订单号"
+                icon-only
+              />
+            </view>
           </view>
         </picker>
         <text v-else class="hint">暂无可开票订单(需已完成且无发票)</text>
@@ -57,6 +66,7 @@ import { useUserStore } from '@/stores/user'
 import { request, parseResponseJson } from '@/utils/request'
 import { API_URL } from '@/config/api'
 import { formatYuan } from '@/utils/format'
+import SqCopyBtn from '@/components/sq-copy-btn.vue'
 
 const userStore = useUserStore()
 
@@ -278,6 +288,22 @@ onMounted(() => {
   margin-bottom: 20rpx;
   border: 1rpx solid rgba(220, 235, 228, 0.65);
 }
+.picker-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12rpx;
+}
+.picker-text {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.picker-copy {
+  flex: 0 0 auto;
+}
 .input {
   border: 1rpx solid var(--c-border);
   border-radius: 16rpx;

+ 387 - 65
sqxp-wx/pages/client/order/detail/detail.vue

@@ -11,6 +11,7 @@ import ProductDetailOverlay from '@/components/client/product-detail-overlay.vue
 import ClientTabBar from '@/components/client/client-tab-bar.vue'
 import ClientSkeleton from '@/components/client/client-skeleton.vue'
 import ClientImageLoader from '@/components/client/client-image-loader.vue'
+import SqCopyBtn from '@/components/sq-copy-btn.vue'
 import { IMG_DEFAULT_PRODUCT, staticAssetUrl } from '@/utils/staticAsset'
 
 const userStore = useUserStore()
@@ -21,6 +22,13 @@ const orderDetail = ref(null)
 const isLoading = ref(false)
 const showProductOverlay = ref(false)
 const overlayProductId = ref('')
+const overlayFallbackProductName = ref('')
+const overlayFallbackMeasureText = ref('')
+const overlayFallbackPrice = ref(null)
+const overlayFallbackImageUrl = ref('')
+const overlayFallbackQuantity = ref(null)
+const overlayFallbackLineAmount = ref(null)
+const showConfirmReceiveDialog = ref(false)
 
 const statusMap = {
   pending_payment: { text: '待支付', color: '#0a9f6d' },
@@ -45,6 +53,20 @@ function unifiedStatus(detail) {
   })
 }
 
+function getStatusClass(status) {
+  const classMap = {
+    pending_payment: 'warning',
+    pending_review_cancel: 'warning',
+    pending_delivery: 'primary',
+    delivering: 'primary',
+    partial_delivered: 'warning',
+    pending_confirm: 'primary',
+    completed: 'success',
+    cancelled: 'info',
+  }
+  return classMap[status] || 'info'
+}
+
 function canPay(detail) {
   if (!detail) return false
   if (detail.orderStatus === 'pending_payment' && detail.payStatus === 'pending_payment') return true
@@ -108,39 +130,56 @@ function formatMeasureText(measureValue, unitName) {
   return unitText ? `${measureText} ${unitText}` : measureText || '-'
 }
 
-function openProductOverlay(productId) {
-  if (!productId) return
-  overlayProductId.value = productId
+function openProductOverlay(item) {
+  if (!item) return
+  overlayProductId.value = item.productId || ''
+  overlayFallbackProductName.value = item.productName || ''
+  overlayFallbackMeasureText.value = formatMeasureText(item.measureValue, item.unitName)
+  const fallbackPrice = item.unitPrice ?? item.price
+  overlayFallbackPrice.value = fallbackPrice == null ? null : Number(fallbackPrice)
+  overlayFallbackImageUrl.value = item.imageUrl || ''
+  overlayFallbackQuantity.value = item.quantity == null ? null : Number(item.quantity)
+  const amt = item.amount ?? item.lineAmount
+  overlayFallbackLineAmount.value = amt == null ? null : Number(amt)
   showProductOverlay.value = true
 }
 
 function closeProductOverlay() {
   showProductOverlay.value = false
+  overlayProductId.value = ''
+  overlayFallbackProductName.value = ''
+  overlayFallbackMeasureText.value = ''
+  overlayFallbackPrice.value = null
+  overlayFallbackImageUrl.value = ''
+  overlayFallbackQuantity.value = null
+  overlayFallbackLineAmount.value = null
 }
 
 function confirmReceive() {
-  uni.showModal({
-    title: '确认收货',
-    content: '确定已收到商品吗?',
-    success: async (res) => {
-      if (!res.confirm) return
-      try {
-        const result = await request({
-          url: `/mini/client/orders/${orderId.value}/confirm-receive`,
-          method: 'POST'
-        })
-        const body = parseResponseJson(result)
-        if (body.code === 200) {
-          uni.showToast({ title: '确认成功', icon: 'success' })
-          void fetchOrderDetail()
-        } else {
-          uni.showToast({ title: body.message || '操作失败', icon: 'none' })
-        }
-      } catch {
-        uni.showToast({ title: '操作失败', icon: 'none' })
-      }
+  showConfirmReceiveDialog.value = true
+}
+
+function closeConfirmReceiveDialog() {
+  showConfirmReceiveDialog.value = false
+}
+
+async function submitConfirmReceive() {
+  try {
+    const result = await request({
+      url: `/mini/client/orders/${orderId.value}/confirm-receive`,
+      method: 'POST'
+    })
+    const body = parseResponseJson(result)
+    if (body.code === 200) {
+      uni.showToast({ title: '确认成功', icon: 'success' })
+      closeConfirmReceiveDialog()
+      void fetchOrderDetail()
+    } else {
+      uni.showToast({ title: body.message || '操作失败', icon: 'none' })
     }
-  })
+  } catch {
+    uni.showToast({ title: '操作失败', icon: 'none' })
+  }
 }
 
 function goPay() {
@@ -164,6 +203,17 @@ const hasDelivererInfo = computed(() => {
 
 const ICON_COORDINATE = staticAssetUrl('coordinate.svg')
 const ICON_LOGISTICS_DISTRIBUTION = staticAssetUrl('logistics-distribution.svg')
+const ICON_SECTION_GOODS = staticAssetUrl('product-icon.svg')
+const ICON_SECTION_ORDER_INFO = staticAssetUrl('dingdanxinxi.svg')
+const ICON_SECTION_AMOUNT = staticAssetUrl('qian.svg')
+function goBack() {
+  uni.navigateBack({
+    delta: 1,
+    fail: () => {
+      uni.switchTab({ url: '/pages/client/order/order' })
+    }
+  })
+}
 
 onLoad((query) => {
   userStore.checkLoginStatus()
@@ -191,9 +241,26 @@ onLoad((query) => {
 
     <template v-else-if="orderDetail">
       <view class="detail-fixed-top">
-        <view class="client-subpage-hero order-detail-hero sq-hero-header sq-hero-header--compact">
-          <text class="client-subpage-hero__sub">订单号 {{ orderDetail.orderNo }}</text>
-          <text class="order-status-badge">{{ statusMap[unifiedStatus(orderDetail)]?.text }}</text>
+        <view class="client-subpage-hero order-detail-hero sq-hero-header sq-hero-header--compact sq-hero-header--client">
+          <view class="hero-top">
+            <view class="hero-header-row">
+              <view class="hero-back-btn" @click="goBack">
+                <text>‹</text>
+              </view>
+              <text class="hero-title hero-title--center">订单详情</text>
+            </view>
+            <view class="hero-order-no hero-order-no--empty">
+              <text class="hero-order-no__text">&nbsp;</text>
+            </view>
+            <view class="hero-meta-row">
+              <text class="hero-order-no__text">订单号:{{ orderDetail.orderNo }}</text>
+              <view
+                :class="['status-tag', 'status-' + getStatusClass(unifiedStatus(orderDetail)), 'order-status-tag']"
+              >
+                <text>{{ statusMap[unifiedStatus(orderDetail)]?.text || unifiedStatus(orderDetail) }}</text>
+              </view>
+            </view>
+          </view>
         </view>
       </view>
 
@@ -244,7 +311,6 @@ onLoad((query) => {
                   <PhoneDialInline
                     v-if="orderDetail.delivererPhone"
                     :phone="orderDetail.delivererPhone"
-                    color="#000000"
                     icon-size="20"
                     expand
                     end-align
@@ -266,14 +332,17 @@ onLoad((query) => {
           <!-- 商品列表 -->
           <view class="goods-section">
             <view class="section-title-row">
-              <text class="section-title">商品清单</text>
+              <view class="section-title-with-icon">
+                <image class="section-title-icon" :src="ICON_SECTION_GOODS" mode="aspectFit" />
+                <text class="section-title">商品清单</text>
+              </view>
               <text class="section-subtitle">点击商品查看详情</text>
             </view>
             <view
               v-for="item in orderDetail.items"
               :key="item.id"
               class="goods-item"
-              @click="openProductOverlay(item.productId)"
+              @click="openProductOverlay(item)"
             >
               <view class="item-card">
                 <ClientImageLoader
@@ -302,11 +371,17 @@ onLoad((query) => {
           <!-- 订单信息 -->
           <view class="order-info-section">
             <view class="section-title-row">
-              <text class="section-title">订单信息</text>
+              <view class="section-title-with-icon">
+                <image class="section-title-icon" :src="ICON_SECTION_ORDER_INFO" mode="aspectFit" />
+                <text class="section-title">订单信息</text>
+              </view>
             </view>
             <view class="info-row">
               <text class="info-label">订单编号</text>
-              <text class="info-value">{{ orderDetail.orderNo }}</text>
+              <view class="info-value-wrap">
+                <text class="info-value">{{ orderDetail.orderNo }}</text>
+                <SqCopyBtn class="info-copy" :text="orderDetail.orderNo" label="订单编号" icon-only />
+              </view>
             </view>
             <view class="info-row">
               <text class="info-label">下单时间</text>
@@ -321,7 +396,10 @@ onLoad((query) => {
           <!-- 金额信息 -->
           <view class="amount-section">
             <view class="section-title-row">
-              <text class="section-title">金额汇总</text>
+              <view class="section-title-with-icon">
+                <image class="section-title-icon" :src="ICON_SECTION_AMOUNT" mode="aspectFit" />
+                <text class="section-title">金额汇总</text>
+              </view>
             </view>
             <view class="amount-row total">
               <text class="amount-label">合计(元)</text>
@@ -333,18 +411,10 @@ onLoad((query) => {
 
       <!-- 底部操作(无按钮时不占位,避免出现白条) -->
       <view v-if="hasBottomActions" class="bottom-actions">
-        <view
-          v-if="canPay(orderDetail)"
-          class="action-btn confirm"
-          @click="goPay"
-        >
+        <view v-if="canPay(orderDetail)" class="action-btn confirm" @click="goPay">
           <text>去支付</text>
         </view>
-        <view
-          v-if="canConfirmReceive(orderDetail)"
-          class="action-btn confirm"
-          @click="confirmReceive"
-        >
+        <view v-if="canConfirmReceive(orderDetail)" class="action-btn confirm" @click="confirmReceive">
           <text>确认收货</text>
         </view>
       </view>
@@ -358,10 +428,37 @@ onLoad((query) => {
       :show="showProductOverlay"
       :product-id="overlayProductId"
       :readonly="true"
+      :fallback-product-name="overlayFallbackProductName"
+      :fallback-measure-text="overlayFallbackMeasureText"
+      :fallback-price="overlayFallbackPrice"
+      :fallback-image-url="overlayFallbackImageUrl"
+      :fallback-quantity="overlayFallbackQuantity"
+      :fallback-line-amount="overlayFallbackLineAmount"
+      fallback-hint-text="此商品目前已售罄"
       tab-bar-active="order"
       @close="closeProductOverlay"
     />
 
+    <view
+      v-if="showConfirmReceiveDialog"
+      class="confirm-receive-dialog-mask"
+      @click="closeConfirmReceiveDialog"
+    >
+      <view class="confirm-receive-dialog" @click.stop>
+        <view class="confirm-receive-dialog__icon">!</view>
+        <text class="confirm-receive-dialog__title">确认收货</text>
+        <text class="confirm-receive-dialog__desc">确认已收到本订单全部商品后,将完成本次订单流程。</text>
+        <view class="confirm-receive-dialog__actions">
+          <view class="confirm-receive-dialog__action confirm-receive-dialog__action--cancel" @click="closeConfirmReceiveDialog">
+            取消
+          </view>
+          <button class="confirm-receive-dialog__action confirm-receive-dialog__action--confirm" @click="submitConfirmReceive">
+            确认收货
+          </button>
+        </view>
+      </view>
+    </view>
+
     <!-- 非 tabBar 子页补充底部导航,避免 H5 仅空白占位 -->
     <ClientTabBar active="order" />
   </view>
@@ -426,16 +523,112 @@ onLoad((query) => {
   position: relative;
 }
 
-.order-status-badge {
+.hero-top {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 10rpx;
+}
+
+.hero-header-row {
+  position: relative;
+  width: 100%;
+  min-height: 52rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.hero-back-btn {
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 44rpx;
+  height: 44rpx;
+  min-width: 44rpx;
+  min-height: 44rpx;
+  border-radius: 12rpx;
+  background: transparent;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.hero-back-btn text {
+  font-size: 32rpx;
+  line-height: 1;
+  color: #ffffff;
+}
+
+.hero-title {
+  font-size: 34rpx;
+  font-weight: 700;
+  color: #ffffff;
+  line-height: 1.2;
+}
+
+.hero-title--center {
+  width: 100%;
+  text-align: center;
+}
+
+.hero-meta-row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12rpx;
+}
+
+.hero-order-no {
   display: inline-flex;
   align-items: center;
-  margin-top: 16rpx;
+  gap: 8rpx;
+  margin-top: 0;
+}
+
+.hero-order-no--empty {
+  width: 100%;
+  min-height: 12rpx;
+}
+
+.hero-order-no__text {
+  font-size: 24rpx;
+  color: rgba(255, 255, 255, 0.92);
+  line-height: 1.35;
+  white-space: nowrap;
+}
+
+.status-tag.order-status-tag {
+  align-self: center;
+  margin-top: 0;
   padding: 8rpx 22rpx;
   border-radius: 999rpx;
-  background: rgba(255, 255, 255, 0.2);
-  color: #ffffff;
   font-size: 24rpx;
   font-weight: 600;
+  display: inline-flex;
+  align-items: center;
+}
+
+.status-tag.status-primary {
+  background: #fff7e2;
+  color: #b7791f;
+}
+
+.status-tag.status-warning {
+  background: #fffbeb;
+  color: #d97706;
+}
+
+.status-tag.status-success {
+  background: #ecfdf5;
+  color: #059669;
+}
+
+.status-tag.status-info {
+  background: #f1f5f9;
+  color: #64748b;
 }
 
 .section-title-row {
@@ -451,6 +644,19 @@ onLoad((query) => {
   color: var(--c-text);
 }
 
+.section-title-with-icon {
+  display: inline-flex;
+  align-items: center;
+  gap: 10rpx;
+  min-width: 0;
+}
+
+.section-title-icon {
+  width: 36rpx;
+  height: 36rpx;
+  flex-shrink: 0;
+}
+
 .section-subtitle {
   font-size: 22rpx;
   color: var(--c-muted);
@@ -468,8 +674,8 @@ onLoad((query) => {
   box-sizing: border-box;
 
   .address-icon {
-    width: 40rpx;
-    height: 40rpx;
+    width: 36rpx;
+    height: 36rpx;
     margin-top: 2rpx;
     margin-right: 20rpx;
     flex-shrink: 0;
@@ -529,8 +735,8 @@ onLoad((query) => {
   }
 
   .deliverer-header-icon {
-    width: 40rpx;
-    height: 40rpx;
+    width: 36rpx;
+    height: 36rpx;
     flex-shrink: 0;
   }
 
@@ -594,7 +800,6 @@ onLoad((query) => {
     }
 
     .deliverer-phone-cell :deep(.phone-dial-inline__text) {
-      color: #000000;
       font-weight: 500;
       font-size: 30rpx;
       text-align: right;
@@ -765,6 +970,26 @@ onLoad((query) => {
     }
   }
 
+  .info-value-wrap {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    gap: 10rpx;
+
+    .info-value {
+      flex: 1;
+      min-width: 0;
+      text-align: right;
+      word-break: break-all;
+    }
+  }
+
+  .info-copy {
+    flex: 0 0 auto;
+  }
+
   .remark-row {
     align-items: flex-start;
 
@@ -811,29 +1036,34 @@ onLoad((query) => {
 
 .bottom-actions {
   position: fixed;
-  bottom: calc(88rpx + env(safe-area-inset-bottom, 0px));
+  /* #ifdef H5 */
+  bottom: calc(88rpx + 10px + env(safe-area-inset-bottom, 0px));
+  padding-bottom: 0;
+  /* #endif */
+  /* #ifndef H5 */
+  bottom: calc(88rpx + env(safe-area-inset-bottom, 0px) + 10px);
+  padding-bottom: 0;
+  /* #endif */
   left: 50%;
   transform: translateX(-50%);
   width: 100%;
   max-width: 840rpx;
-  min-height: 104rpx;
-  background: #ffffff;
+  min-height: 88rpx;
   display: flex;
   align-items: center;
-  justify-content: flex-end;
+  justify-content: center;
   flex-wrap: wrap;
-  gap: 12rpx;
-  padding: 14rpx 24rpx;
-  padding-bottom: calc(14rpx + env(safe-area-inset-bottom));
-  border-top: 1rpx solid var(--c-border);
-  box-shadow: 0 -6rpx 18rpx rgba(10, 159, 109, 0.06);
+  gap: 0;
+  padding: 0 24rpx;
+  border: none;
+  box-shadow: none;
   z-index: 520;
 
   .action-btn {
-    min-width: 220rpx;
-    padding: 0 32rpx;
-    height: 76rpx;
-    border-radius: 38rpx;
+    width: 100%;
+    max-width: 640rpx;
+    height: 88rpx;
+    border-radius: 44rpx;
     display: flex;
     align-items: center;
     justify-content: center;
@@ -845,6 +1075,7 @@ onLoad((query) => {
 
     &.confirm {
       background: linear-gradient(145deg, var(--c-barley-yellow), #e4b458);
+      box-shadow: 0 10rpx 24rpx rgba(228, 180, 88, 0.26);
       text {
         color: #ffffff;
         font-weight: 700;
@@ -852,4 +1083,95 @@ onLoad((query) => {
     }
   }
 }
+
+.confirm-receive-dialog-mask {
+  position: fixed;
+  inset: 0;
+  z-index: 800;
+  background: rgba(18, 28, 24, 0.45);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40rpx;
+  box-sizing: border-box;
+}
+
+.confirm-receive-dialog {
+  width: 100%;
+  max-width: 620rpx;
+  background: #ffffff;
+  border-radius: 26rpx;
+  padding: 40rpx 34rpx 32rpx;
+  box-sizing: border-box;
+  box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.15);
+}
+
+.confirm-receive-dialog__icon {
+  width: 76rpx;
+  height: 76rpx;
+  margin: 0 auto 20rpx;
+  border-radius: 50%;
+  background: #fff9ec;
+  color: #d97706;
+  font-size: 42rpx;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.confirm-receive-dialog__title {
+  display: block;
+  text-align: center;
+  color: var(--c-text);
+  font-size: 34rpx;
+  font-weight: 700;
+}
+
+.confirm-receive-dialog__desc {
+  display: block;
+  margin-top: 14rpx;
+  text-align: center;
+  color: var(--c-muted);
+  font-size: 26rpx;
+  line-height: 1.6;
+}
+
+.confirm-receive-dialog__actions {
+  margin-top: 28rpx;
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  column-gap: 16rpx;
+}
+
+.confirm-receive-dialog__action {
+  width: 100%;
+  min-height: 76rpx;
+  border-radius: 999rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  font-weight: 600;
+  border: none;
+  box-sizing: border-box;
+}
+
+.confirm-receive-dialog__action--cancel {
+  background: #f4f7f6;
+  color: #5b6b64;
+  border: 1rpx solid #ddebe5;
+}
+
+.confirm-receive-dialog__action--confirm {
+  background: linear-gradient(145deg, var(--c-barley-yellow), #e4b458);
+  color: #ffffff;
+}
+</style>
+
+<style lang="scss">
+page {
+  height: 100%;
+  background-color: #f5f8f7;
+}
 </style>

+ 60 - 13
sqxp-wx/pages/client/order/order.vue

@@ -1,6 +1,6 @@
 <template>
   <view class="order-page client-theme">
-    <view class="order-top sq-hero-header sq-hero-header--compact">
+    <view class="order-top sq-hero-header sq-hero-header--compact sq-hero-header--client">
       <view class="order-top-content">
         <text class="order-page-title">我的订单</text>
         <scroll-view scroll-x class="order-tabs-scroll" :show-scrollbar="false" :enable-flex="true">
@@ -55,7 +55,11 @@
         @click="goToDetail(order.id)"
       >
         <view class="order-header">
-          <text class="order-no">订单号:{{ order.orderNo }}</text>
+          <view class="order-no-wrap">
+            <image class="order-no-icon" :src="ICON_ORDER_NO" mode="aspectFit" />
+            <text class="order-no">订单号:{{ order.orderNo }}</text>
+            <SqCopyBtn class="order-no-copy" :text="order.orderNo" label="订单号" icon-only />
+          </view>
           <view :class="['status-tag', 'status-' + getOrderStatusClass(order)]">
             <text>{{ getOrderStatusText(order) }}</text>
           </view>
@@ -132,12 +136,14 @@ import { request, parseResponseJson } from '@/utils/request'
 import { formatYuan } from '@/utils/format'
 import { formatDateTime } from '@/utils/date'
 import { getClientOrderUnifiedStatus } from '@/utils/client-order-status'
-import { IMG_DEFAULT_PRODUCT, IMG_EMPTY } from '@/utils/staticAsset'
+import { IMG_DEFAULT_PRODUCT, IMG_EMPTY, staticAssetUrl } from '@/utils/staticAsset'
 import ClientTabBar from '@/components/client/client-tab-bar.vue'
 import ClientSkeleton from '@/components/client/client-skeleton.vue'
 import ClientImageLoader from '@/components/client/client-image-loader.vue'
+import SqCopyBtn from '@/components/sq-copy-btn.vue'
 
 const userStore = useUserStore()
+const ICON_ORDER_NO = staticAssetUrl('dingdanxinxi.svg')
 
 const TAB_KEYS = ['all', 'pending_payment', 'pending_confirm', 'completed', 'cancelled']
 
@@ -452,16 +458,57 @@ onShow(() => {
     align-items: center;
     margin-bottom: 16rpx;
 
+    .order-no-wrap {
+      flex: 0 1 auto;
+      min-width: 0;
+      display: flex;
+      align-items: center;
+      gap: 4rpx;
+      margin-right: 16rpx;
+      max-width: calc(100% - 170rpx);
+    }
+
     .order-no {
-      flex: 1;
+      flex: 0 1 auto;
       min-width: 0;
       font-size: 26rpx;
       font-weight: 600;
+      line-height: 1.2;
       color: var(--c-text);
       overflow: hidden;
       text-overflow: ellipsis;
       white-space: nowrap;
-      margin-right: 16rpx;
+      width: auto;
+      max-width: 100%;
+    }
+
+    .order-no-icon {
+      width: 32rpx;
+      height: 32rpx;
+      flex: 0 0 auto;
+    }
+
+    .order-no-copy {
+      flex: 0 0 auto;
+      display: inline-flex;
+      align-items: center;
+      align-self: center;
+      background: transparent !important;
+      border: none !important;
+      box-shadow: none !important;
+      padding: 0 !important;
+      width: 32rpx !important;
+      height: 32rpx !important;
+      min-width: 32rpx !important;
+      min-height: 32rpx !important;
+      border-radius: 0 !important;
+      justify-content: center;
+    }
+
+    /* 覆盖 SqCopyBtn 组件 icon-only 默认尺寸 */
+    .order-no-copy :deep(.sq-copy-btn__icon) {
+      width: 22rpx !important;
+      height: 22rpx !important;
     }
 
     .status-tag {
@@ -473,23 +520,23 @@ onShow(() => {
       align-items: center;
 
       &.status-primary {
-        background: var(--c-brand-green-soft);
-        color: var(--c-brand-green);
+        background: #fff7e2;
+        color: #b7791f;
       }
 
       &.status-warning {
-        background: #fff4dd;
-        color: #cf8a19;
+        background: #fffbeb;
+        color: #d97706;
       }
 
       &.status-success {
-        background: var(--c-brand-green-soft);
-        color: var(--c-brand-green);
+        background: #ecfdf5;
+        color: #059669;
       }
 
       &.status-info {
-        background: #f4f4f5;
-        color: var(--c-muted);
+        background: #f1f5f9;
+        color: #64748b;
       }
     }
   }

+ 29 - 1
sqxp-wx/pages/client/order/pay/pay.vue

@@ -17,7 +17,10 @@
     </view>
     <view v-else class="card">
       <text class="label">订单号</text>
-      <text class="value">{{ orderNo || '—' }}</text>
+      <view class="value-row">
+        <text class="value value--row">{{ orderNo || '—' }}</text>
+        <SqCopyBtn class="copy" :text="orderNo" label="订单号" icon-only />
+      </view>
       <text class="label mt">下单时间</text>
       <text class="value">{{ formatDateTime(createAt) || '—' }}</text>
       <text class="label mt">应付金额(元)</text>
@@ -51,6 +54,7 @@ import { formatYuan } from '@/utils/format'
 import { formatDateTime } from '@/utils/date'
 import { getClientOrderUnifiedStatus } from '@/utils/client-order-status'
 import ClientSkeleton from '@/components/client/client-skeleton.vue'
+import SqCopyBtn from '@/components/sq-copy-btn.vue'
 
 const userStore = useUserStore()
 const orderId = ref('')
@@ -359,6 +363,22 @@ onLoad((query) => {
   display: block;
   margin-top: 8rpx;
 }
+.value-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12rpx;
+  margin-top: 8rpx;
+}
+.value--row {
+  flex: 1;
+  min-width: 0;
+  margin-top: 0;
+  word-break: break-all;
+}
+.copy {
+  flex: 0 0 auto;
+}
 .mt {
   margin-top: 28rpx;
 }
@@ -406,6 +426,14 @@ onLoad((query) => {
   box-shadow: 0 2rpx 10rpx rgba(10, 159, 109, 0.1);
 }
 
+/* 微信小程序按钮点击态会挂 button-hover,需锁定文字色避免变白 */
+.btn.secondary:active,
+.btn.secondary.button-hover {
+  color: var(--c-brand-green) !important;
+  background: #f3fbf7 !important;
+  border-color: rgba(10, 159, 109, 0.55) !important;
+}
+
 :deep(.btn.van-button) {
   border: none !important;
   box-shadow: none !important;

+ 1 - 1
sqxp-wx/pages/client/profile/profile.vue

@@ -5,7 +5,7 @@
     </view>
     <scroll-view v-else scroll-y class="profile-scroll" :show-scrollbar="true">
       <!-- 对齐 sanqiang-client-profile-ui-demo.html .header -->
-      <view class="profile-header sq-hero-header sq-hero-header--compact">
+      <view class="profile-header sq-hero-header sq-hero-header--compact sq-hero-header--client">
         <view class="profile-header-content">
           <view class="user-row">
             <view class="avatar">

+ 23 - 1
sqxp-wx/pages/client/refund/refund.vue

@@ -4,6 +4,7 @@ import { useUserStore } from '@/stores/user'
 import { request, parseResponseJson } from '@/utils/request'
 import { API_URL } from '@/config/api'
 import { formatYuan } from '@/utils/format'
+import SqCopyBtn from '@/components/sq-copy-btn.vue'
 
 const userStore = useUserStore()
 
@@ -70,7 +71,10 @@ onMounted(() => {
             <text class="no">{{ row.refundNo || row.id }}</text>
             <text class="st">{{ statusText[row.status] || row.status }}</text>
           </view>
-          <text class="sub">订单 {{ row.orderNo || '—' }}</text>
+          <view class="sub-row">
+            <text class="sub">订单 {{ row.orderNo || '—' }}</text>
+            <SqCopyBtn v-if="row.orderNo" class="copy" :text="row.orderNo" label="订单号" icon-only />
+          </view>
           <text class="amt">¥{{ formatYuan(row.refundAmount) }}</text>
           <text class="time">{{ row.refundTime || row.createAt }}</text>
         </view>
@@ -129,6 +133,24 @@ onMounted(() => {
   color: var(--c-muted);
   margin-top: 12rpx;
 }
+.sub-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12rpx;
+  margin-top: 12rpx;
+}
+.sub-row .sub {
+  flex: 1;
+  min-width: 0;
+  margin-top: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.copy {
+  flex: 0 0 auto;
+}
 .amt {
   display: block;
   font-size: 32rpx;

+ 20 - 1
sqxp-wx/pages/client/settlement/settlement.vue

@@ -11,7 +11,10 @@
         <view v-else-if="list.length === 0" class="hint">暂无结算记录</view>
         <view v-for="row in list" :key="row.id" class="card" hover-class="card--pressed" :hover-stay-time="100">
       <view class="line">
-        <text class="order">订单 {{ row.orderNo || row.id.slice(0, 8) }}</text>
+        <view class="order-wrap">
+          <text class="order">订单 {{ row.orderNo || row.id.slice(0, 8) }}</text>
+          <SqCopyBtn v-if="row.orderNo" class="copy" :text="row.orderNo" label="订单号" icon-only />
+        </view>
         <text class="amt">¥{{ formatYuan(row.amount) }}</text>
       </view>
       <text class="sub">{{ row.settlementMethod || '—' }} · {{ row.settlementTime || row.createAt }}</text>
@@ -29,6 +32,7 @@ import { useUserStore } from '@/stores/user'
 import { request, parseResponseJson } from '@/utils/request'
 import { API_URL } from '@/config/api'
 import { formatYuan } from '@/utils/format'
+import SqCopyBtn from '@/components/sq-copy-btn.vue'
 
 const userStore = useUserStore()
 
@@ -121,9 +125,24 @@ onMounted(() => {
   justify-content: space-between;
   align-items: center;
 }
+.order-wrap {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  align-items: center;
+  gap: 10rpx;
+}
 .order {
+  flex: 1;
+  min-width: 0;
   font-size: 28rpx;
   color: var(--c-text);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.copy {
+  flex: 0 0 auto;
 }
 .amt {
   font-size: 30rpx;

BIN
sqxp-wx/static/tabbar/购物车-active.png


BIN
sqxp-wx/static/tabbar/购物车.png


+ 23 - 11
sqxp-wx/stores/user.js

@@ -47,19 +47,31 @@ export const useUserStore = defineStore('user', () => {
     const savedToken = uni.getStorageSync(TOKEN_KEY)
     const savedUserInfo = uni.getStorageSync(USER_INFO_KEY)
     const savedRefreshToken = uni.getStorageSync(REFRESH_TOKEN_KEY)
-    if (savedToken && savedUserInfo) {
-      token.value = typeof savedToken === 'string' ? savedToken.trim() : String(savedToken)
-      refreshToken.value = typeof savedRefreshToken === 'string' ? savedRefreshToken : ''
-      if (typeof savedUserInfo === 'string') {
-        try {
-          userInfo.value = normalizeUserInfo(JSON.parse(savedUserInfo))
-        } catch (error) {
-          userInfo.value = null
-        }
-      } else {
-        userInfo.value = normalizeUserInfo(savedUserInfo)
+    const normalizedToken =
+      typeof savedToken === 'string' ? savedToken.trim() : savedToken ? String(savedToken).trim() : ''
+
+    let normalizedUserInfo = null
+    if (typeof savedUserInfo === 'string') {
+      try {
+        normalizedUserInfo = normalizeUserInfo(JSON.parse(savedUserInfo))
+      } catch (error) {
+        normalizedUserInfo = null
       }
+    } else if (savedUserInfo && typeof savedUserInfo === 'object') {
+      normalizedUserInfo = normalizeUserInfo(savedUserInfo)
+    }
+
+    if (normalizedToken && normalizedUserInfo) {
+      token.value = normalizedToken
+      refreshToken.value = typeof savedRefreshToken === 'string' ? savedRefreshToken : ''
+      userInfo.value = normalizedUserInfo
+      return
     }
+
+    // 本地无有效登录态时,必须同步清空内存态,避免登录页 onShow 误判已登录后跳回首页
+    token.value = ''
+    refreshToken.value = ''
+    userInfo.value = null
   }
 
   function setLoginInfo(data) {

+ 5 - 0
sqxp-wx/styles/index.scss

@@ -565,6 +565,11 @@ view[class*='fee'],
   padding-top: calc(72rpx + var(--sq-safe-top, env(safe-area-inset-top, 0px)));
 }
 
+/* 客户端 tab 页面头部:不占胶囊右侧安全位,避免右侧内容被额外挤压 */
+.sq-hero-header--client {
+  padding-right: 32rpx;
+}
+
 .sq-hero-back-row {
   display: flex;
   align-items: center;

+ 12 - 2
sqxp-wx/utils/request.js

@@ -2,6 +2,16 @@ import { API_BASE, TOKEN_KEY, REFRESH_TOKEN_KEY, USER_INFO_KEY } from '@/config/
 
 let clearingAuth = false
 
+function createUnauthorizedError(rawUrl = '') {
+  const loginApiPattern = /\/mini\/auth\/login(?:$|\?)/
+  const message = loginApiPattern.test(String(rawUrl))
+    ? '用户名或密码错误,请重新输入'
+    : '登录状态已失效,请重新登录'
+  const error = new Error(message)
+  error.name = 'UnauthorizedError'
+  return error
+}
+
 async function onHttpUnauthorized() {
   if (clearingAuth) return
   clearingAuth = true
@@ -43,7 +53,7 @@ function isBizUnauthorized(data) {
 
 export function request(options) {
   if (clearingAuth) {
-    return Promise.reject(new Error('Unauthorized'))
+    return Promise.reject(createUnauthorizedError(options?.url))
   }
   const rawToken = uni.getStorageSync(TOKEN_KEY)
   const token = typeof rawToken === 'string' ? rawToken.trim() : ''
@@ -64,7 +74,7 @@ export function request(options) {
       success: (res) => {
         if (res.statusCode === 401 || isBizUnauthorized(res.data)) {
           onHttpUnauthorized()
-          reject(new Error('Unauthorized'))
+          reject(createUnauthorizedError(rawUrl))
           return
         }
         resolve(res)

+ 2 - 2
web-admin/vite.config.ts

@@ -33,13 +33,13 @@ export default ({ mode }: { mode: string }) => {
         },
         // Web端接口代理 - 转发到后端 /api 前缀
         '/web': {
-          target: 'https://www.chengyantek.cn/sqxp-api',
+          target: 'http://192.168.2.110:8000/api',
           changeOrigin: true,
           rewrite: (path) => `/api${path}`
         },
         // 小程序端接口代理
         '/mini': {
-          target: 'https://www.chengyantek.cn/sqxp-api',
+          target: 'http://192.168.2.110:8000/api',
           changeOrigin: true,
           rewrite: (path) => `/api${path}`
         }