Compare commits

..

9 Commits

50 changed files with 6775 additions and 684 deletions

26
api/homeService.js Normal file
View File

@ -0,0 +1,26 @@
export const apiArr = {
// 到家服务
categoryList: "/api/v2/wechat/service/category/list", // 服务分类列表
serviceList: "/api/v2/wechat/service/list", // 服务商品列表
serviceInfo: "/api/v2/wechat/service/info", // 服务详情(含SKU+师傅)
orderCreate: "/api/v2/wechat/service/order/create", // 下单(定金单)
orderList: "/api/v2/wechat/service/order/list", // 我的服务订单列表
orderInfo: "/api/v2/wechat/service/order/info", // 服务订单详情
orderCancel: "/api/v2/wechat/service/order/cancel", // 取消服务订单
preorder: "/api/v2/wechat/service/order/preorder", // 预下单(拉卡拉)
tradeQuery: "/api/v2/wechat/service/order/trade-query", // 查单
// 师傅端
workerOrderList: "/api/v2/wechat/service/worker/order-list", // 师傅服务单列表
workerOrderStatus: "/api/v2/wechat/service/worker/order-status", // 师傅更新状态
workerExtraOrder: "/api/v2/wechat/service/worker/extra-order", // 师傅代客补差单
workerMyInfo: "/api/v2/wechat/service/worker/my-info", // 解析当前用户的师傅信息
// 家政合同(客户端)
contractList: "/api/v2/wechat/service/contract/list", // 我的家政合同列表
contractInfo: "/api/v2/wechat/service/contract/info", // 家政合同详情
// 月账单
workerMonthlyBillList: "/api/v2/wechat/service/worker/monthly-bill-list", // 师傅月账单列表
workerPushBill: "/api/v2/wechat/service/worker/push-bill", // 师傅推送/拒绝月账单
userMonthlyBillList: "/api/v2/wechat/service/user/monthly-bill-list", // 用户月账单列表
userMonthlyBillPreOrder: "/api/v2/wechat/service/user/monthly-bill/preorder", // 用户月账单预下单
userMonthlyBillTradeQuery: "/api/v2/wechat/service/user/monthly-bill/trade-query", // 用户月账单查单
}

View File

@ -2,6 +2,7 @@ export const apiArr = {
getCateList: "/api/v2/wechat/merchant-cate-crud/list", //商家分类列表
getMerchantList: "/api/v2/wechat/merchant-info-crud/page", //商家列表
getHomeBanner: "/api/v2/wechat/home-banner-region-crud/page", //获取首页banner及其广告
getHomeBannerMulti: "/api/v2/wechat/home-banner-region-crud/multi", //按广告位批量获取首页广告(一次返回多个 ad_position
getButtonNum:"/api/v2/wechat/nav-display-crud/info",//获取首页button的行数 数量
getHomeButton:"/api/v2/wechat/home-button-region-crud/page", //获取首页button

View File

@ -3,6 +3,7 @@ export const apiArr = {
getMerchantList:"/api/v2/wechat/merchant-info-crud/page",//商家列表
createComment:"/api/v2/wechat/merchant-evaluation-crud/creat",//创建用户评价
getMerchantInfo:"/api/v2/wechat/merchant-info-crud/info",//获取商家信息详情
getShopActivityList:"/api/v2/wechat/commodity/shop-activity/list",//好店详情页-店铺活动商品列表
getMerchantComment:"/api/v2/wechat/merchant-evaluation-crud/page",//获取商家评价
merChantCommentLike:"/api/v2/wechat/merchant-evaluation-like-crud/creat",//点赞
merChantCommentUnlike:"/api/v2/wechat/merchant-evaluation-like-crud/del",//取消点赞
@ -26,4 +27,5 @@ export const apiArr = {
createStore:"/api/v2/wechat/store-info-crud/creat",//门店信息创建
ocrRecognize:"/api/v1/wechat/oss/ocr-recognize",//证件 OCR 自动识别
};

107
components/PrivateImage.vue Normal file
View File

@ -0,0 +1,107 @@
<template>
<view class="private-image">
<image v-if="signedUrl" :src="signedUrl" :mode="mode" :style="{ width: width, height: height }"
@click="onClick" @error="onError" />
<view v-else-if="loading" class="ph">加载中</view>
<view v-else-if="error" class="ph err">{{ error }}</view>
<view v-else-if="!path" class="ph">未上传</view>
</view>
</template>
<script>
// PrivateImage
//
//
// <PrivateImage :path="form.id_card_front" width="200rpx" height="120rpx" />
//
// targetUserId object key
import {
signPrivateView
} from '@/utils/uploadOSS.js'
export default {
name: 'PrivateImage',
props: {
path: {
type: String,
default: ''
},
width: {
type: String,
default: '200rpx'
},
height: {
type: String,
default: '120rpx'
},
mode: {
type: String,
default: 'aspectFill'
},
// mount click
trigger: {
type: String,
default: 'mount'
}
},
data() {
return {
signedUrl: '',
loading: false,
error: ''
}
},
watch: {
path: {
immediate: true,
handler(v) {
if (v && this.trigger === 'mount') this.fetch()
else this.signedUrl = ''
}
}
},
methods: {
async fetch() {
if (!this.path) return
this.loading = true
this.error = ''
try {
const res = await signPrivateView(this.path)
this.signedUrl = res.url
} catch (e) {
this.error = e.message || '加载失败'
} finally {
this.loading = false
}
},
onClick() {
if (this.signedUrl) {
uni.previewImage({
urls: [this.signedUrl]
})
} else if (this.trigger === 'click') {
this.fetch()
}
},
onError() {
this.error = '图片加载失败'
this.signedUrl = ''
}
}
}
</script>
<style scoped>
.private-image .ph {
display: inline-block;
text-align: center;
color: #999;
font-size: 22rpx;
padding: 16rpx;
border: 2rpx dashed #ccc;
}
.private-image .ph.err {
color: #f56c6c;
}
</style>

View File

@ -167,18 +167,21 @@ export default {
// pathscene
const pathParts = res.path.split('?');
if (pathParts.length > 1) {
const queryParams = pathParts[1].split('&');
for (const param of queryParams) {
const [key, value] = param.split('=');
if (key === 'scene') {
const params = {
id: value
}
uni.setStorageSync('merchantInfo', params);
NavgateTo('/packages/localLife/detail/index')
break;
}
}
if (pathParts[1].startsWith('scene=')){
const pathPart = decodeURIComponent(pathParts[1].substring(6))
const queryParams = pathPart.split('&');
for (const param of queryParams) {
const [key, value] = param.split('=');
if (key === 'id') {
const params = {
id: value
}
uni.setStorageSync('merchantInfo', params);
NavgateTo('/packages/localLife/detail/index')
break;
}
}
}
}
}
},

View File

@ -0,0 +1,84 @@
# 首页 Banner / 广告位 跳转现状梳理
> 状态现状分析仅梳理代码未改。日期2026-06-03
> 范围:小程序 `pages/index/index.vue` 首页各广告位的点击跳转逻辑,对照后端配置。
---
## 一、后端是怎么配置跳转的
每条 banner/广告(表 `home_banner_region`)、金刚位(表 `home_button_region`)都有统一的跳转配置字段,核心是 **`promotion_type`(推广类型)**
| promotion_type | 含义 | 配套字段 |
|---|---|---|
| 1 | 无跳转 | — |
| 2 | 选择活动 | 关联 `advertisement_goods`(按 banner id |
| 3 | 跳转本程序页面 | `link_url` |
| 4 | 跳转其他小程序 | `appid` + `link_url` |
| 5 | 选择商品 | `advertisement_goods[].goods_id` |
- 后台创建/编辑接口(`api/admin/v2/crud_home_banner_region.go`)用 `promotion_type` + `required-if` 校验对应字段,例如 type=4 必填 appid、type=3/4 必填 link_url。
- `home_button_region`(金刚位)字段相同,也有 `promotion_type` / `link_url` / `appid`
- type=2/5 的落地:`/packages/advertising/index/index?id=<banner.id>` 是“广告聚合落地页”,传 banner 自身 id 进去,页面内部拉该 banner 关联的 `advertisement_goods` 商品列表展示,并可进入 `goodsDetail`
---
## 二、前端实际怎么跳的(问题所在)
首页有 4 个不同的点击处理函数各管一片,**几乎都没读 `promotion_type`**
### 1. `headerServerClick`(金刚位 tabList、homeLeftList 用)
- 文件:`pages/index/index.vue`(搜 `headerServerClick(e)`
- 只看 `link_url``appid`**完全忽略 `promotion_type`**。
- 逻辑:无 `link_url` → 弹“此功能暂未开通”;有 `appid` → 跳小程序;否则当本程序路径 `NavgateTo`
- **后果**type=2活动、type=5商品的配置它不认 → 落到“没 link_url”分支 → 弹“暂未开通”。
### 2. `toAdvertisingView`(广告横幅 serverLeft、serverRight[0] 用)
- 文件:`pages/index/index.vue:357` 附近
- **写死**跳 `/packages/advertising/index/index?id=xxx`,不看 `promotion_type`,一律进广告落地页。
### 3. `goToPurify`serverRight 非首个用,模板 `index.vue:101`
- 文件:`pages/index/index.vue:363` 附近
- **彻底写死**:固定跳净水小程序 `appId=wx77b22c0a0018e580`path/appId 全硬编码,传入的 `item` 没用到,与后台配置零关系。
- 模板:`@tap="index === 0 ? toAdvertisingView(serverRightList) : goToPurify(item)"` —— serverRight 第一个跳广告页,其余全跳净水小程序。
### 4. `headerServerClick2`(中部 serverItem / homeRightList1,2 用)
- 文件:`pages/index/index.vue`(搜 `headerServerClick2(e)`
- 不跳外链,只按 `title` 匹配商家分类做筛选,或固定跳本地生活页。**注意:这一处可能是“按分类筛选”的有意设计,不一定算 bug。**
### 5. 顶部轮播 Banner位置 1模板 `index.vue:68`
- 点击事件被**注释掉了**`<!-- <swiper-item ... @click="headerServerClick(item)"> -->`),顶部 banner 现在点击无反应。
---
## 三、根因
后台用统一的 `promotion_type` 模型配跳转,前端却是早期“按位置分别写死”的处理函数拼起来的:既没统一读 `promotion_type`,部分位置还硬编码了目标(广告页、净水小程序)。所以出现“很多跳转和后台配置不符”——不是数据错,是前端没按配置解析。
---
## 四、建议修法(未实施,待决策)
做一个统一跳转函数 `handleAdJump(item)`,严格按 `promotion_type` 分发:
```
switch promotion_type:
1: 不跳
2: NavgateTo('/packages/advertising/index/index?id=' + item.id) // 活动
3: link_url 跳本程序页面(兼容 shopEnter 特例 / H5 / APP webview
4: navigateToMiniProgram(appid, link_url) // 其他小程序
5: NavgateTo('/packages/advertising/index/index?id=' + item.id) // 商品(同活动,落地页内展示)
其他: 弹“暂未开通”
```
然后把首页所有广告位的 `@click`/`@tap` 都换成 `handleAdJump(item)`
- 顶部轮播 banner取消注释`handleAdJump`
- 金刚位 tabList、homeLeftList`headerServerClick``handleAdJump`
- serverLeft、serverRight`toAdvertisingView` / `goToPurify``handleAdJump`
### 实施前需确认的点
1. `headerServerClick2`(中部 serverItem 按 title 筛分类)是否要保留分类筛选语义?还是也改成按 `promotion_type` 跳?
2. 净水小程序跳转:是想**保留写死**,还是改由后台某条 banner 配 `type=4 + 该 appid` 来控制(更灵活,去掉硬编码)?
3. type=2/5 是否都统一进广告落地页 `/packages/advertising/index/index?id=banner.id`(当前分析结论是“是”)。
> 相关:`promotion_type` 字段定义见 `huichang-server/internal/model/entity/home_banner_region.go``home_button_region.go`;广告落地页 `uniapp-ZHSQ/packages/advertising/index/index.vue`

32
main.js
View File

@ -10,6 +10,22 @@ Vue.config.productionTip = false
import uView from 'uni_modules/uview-ui';
Vue.use(uView);
// 全局分享 mixin - 所有页面默认开启分享到微信好友和朋友圈
Vue.mixin({
onShareAppMessage() {
return {
title: '汇商生活家',
path: '/pages/index/index'
}
},
onShareTimeline() {
return {
title: '汇商生活家',
query: ''
}
}
})
// 导入工具函数
import { processImageUrl } from './utils/index.js'
@ -39,6 +55,22 @@ const app = createApp(App)
// 安装 uView
app.use(uView)
// 全局分享 mixin - 所有页面默认开启分享到微信好友和朋友圈
app.mixin({
onShareAppMessage() {
return {
title: '汇商生活家',
path: '/pages/index/index'
}
},
onShareTimeline() {
return {
title: '汇商生活家',
query: ''
}
}
})
// 导入工具函数
import { processImageUrl } from './utils/index.js'

839
package-lock.json generated
View File

@ -1,7 +1,7 @@
{
"name": "支付键盘、数字键盘、付款键盘、密码键盘",
"version": "1.0.1",
"lockfileVersion": 3,
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
@ -1168,5 +1168,842 @@
"node": ">=0.4"
}
}
},
"dependencies": {
"@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
},
"@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="
},
"@babel/parser": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"requires": {
"@babel/types": "^7.28.4"
}
},
"@babel/types": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"requires": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
},
"@vue/compiler-core": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz",
"integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==",
"requires": {
"@babel/parser": "^7.28.3",
"@vue/shared": "3.5.21",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"@vue/compiler-dom": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz",
"integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==",
"requires": {
"@vue/compiler-core": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"@vue/compiler-sfc": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz",
"integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==",
"requires": {
"@babel/parser": "^7.28.3",
"@vue/compiler-core": "3.5.21",
"@vue/compiler-dom": "3.5.21",
"@vue/compiler-ssr": "3.5.21",
"@vue/shared": "3.5.21",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.18",
"postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"@vue/compiler-ssr": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz",
"integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==",
"requires": {
"@vue/compiler-dom": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"@vue/reactivity": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz",
"integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==",
"requires": {
"@vue/shared": "3.5.21"
}
},
"@vue/runtime-core": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.21.tgz",
"integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==",
"requires": {
"@vue/reactivity": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"@vue/runtime-dom": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz",
"integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==",
"requires": {
"@vue/reactivity": "3.5.21",
"@vue/runtime-core": "3.5.21",
"@vue/shared": "3.5.21",
"csstype": "^3.1.3"
}
},
"@vue/server-renderer": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.21.tgz",
"integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==",
"requires": {
"@vue/compiler-ssr": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"@vue/shared": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz",
"integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw=="
},
"abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"requires": {
"event-target-shim": "^5.0.0"
}
},
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
},
"dependencies": {
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"callback-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/callback-stream/-/callback-stream-1.1.0.tgz",
"integrity": "sha512-sAZ9kODla+mGACBZ1IpTCAisKoGnv6PykW7fPk1LrM+mMepE18Yz0515yoVcrZy7dQsTUp3uZLQ/9Sx1RnLoHw==",
"requires": {
"inherits": "^2.0.1",
"readable-stream": "> 1.0.0 < 3.0.0"
}
},
"commist": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz",
"integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==",
"requires": {
"leven": "^2.1.0",
"minimist": "^1.1.0"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"d": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"requires": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
}
},
"debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"requires": {
"ms": "^2.1.3"
}
},
"duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
"requires": {
"end-of-stream": "^1.0.0",
"inherits": "^2.0.1",
"readable-stream": "^2.0.0",
"stream-shift": "^1.0.0"
}
},
"end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"requires": {
"once": "^1.4.0"
}
},
"entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
},
"es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"requires": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
"esniff": "^2.0.1",
"next-tick": "^1.1.0"
}
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"requires": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"es6-map": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz",
"integrity": "sha512-mz3UqCh0uPCIqsw1SSAkB/p0rOzF/M0V++vyN7JqlPtSW/VsYgQBvVvqMLmfBuyMzTpLnNqi6JmcSizs4jy19A==",
"requires": {
"d": "1",
"es5-ext": "~0.10.14",
"es6-iterator": "~2.0.1",
"es6-set": "~0.1.5",
"es6-symbol": "~3.1.1",
"event-emitter": "~0.3.5"
}
},
"es6-set": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.6.tgz",
"integrity": "sha512-TE3LgGLDIBX332jq3ypv6bcOpkLO0AslAQo7p2VqX/1N46YNsvIWgvjojjSEnWEGWMhr1qUbYeTSir5J6mFHOw==",
"requires": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"es6-iterator": "~2.0.3",
"es6-symbol": "^3.1.3",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
}
},
"es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"requires": {
"d": "^1.0.2",
"ext": "^1.7.0"
}
},
"esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"requires": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
}
},
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"requires": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"ext": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"requires": {
"type": "^2.7.2"
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==",
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
}
},
"glob-stream": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz",
"integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==",
"requires": {
"extend": "^3.0.0",
"glob": "^7.1.1",
"glob-parent": "^3.1.0",
"is-negated-glob": "^1.0.0",
"ordered-read-streams": "^1.0.0",
"pumpify": "^1.3.5",
"readable-stream": "^2.1.5",
"remove-trailing-separator": "^1.0.1",
"to-absolute-glob": "^2.0.0",
"unique-stream": "^2.0.2"
}
},
"help-me": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz",
"integrity": "sha512-P/IZ8yOMne3SCTHbVY429NZ67B/2bVQlcYGZh2iPPbdLrEQ/qY5aGChn0YTDmt7Sb4IKRI51fypItav+lNl76w==",
"requires": {
"callback-stream": "^1.0.2",
"glob-stream": "^6.1.0",
"through2": "^2.0.1",
"xtend": "^4.0.0"
}
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"is-absolute": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
"integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
"requires": {
"is-relative": "^1.0.0",
"is-windows": "^1.0.1"
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
},
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==",
"requires": {
"is-extglob": "^2.1.0"
}
},
"is-negated-glob": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
"integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug=="
},
"is-relative": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
"integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
"requires": {
"is-unc-path": "^1.0.0"
}
},
"is-unc-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
"integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
"requires": {
"unc-path-regex": "^0.1.2"
}
},
"is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
},
"leven": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz",
"integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="
},
"magic-string": {
"version": "0.30.19",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"requires": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
},
"mqtt": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-3.0.0.tgz",
"integrity": "sha512-0nKV6MAc1ibKZwaZQUTb3iIdT4NVpj541BsYrqrGBcQdQ7Jd0MnZD1/6/nj1UFdGTboK9ZEUXvkCu2nPCugHFA==",
"requires": {
"base64-js": "^1.3.0",
"commist": "^1.0.0",
"concat-stream": "^1.6.2",
"end-of-stream": "^1.4.1",
"es6-map": "^0.1.5",
"help-me": "^1.0.1",
"inherits": "^2.0.3",
"minimist": "^1.2.0",
"mqtt-packet": "^6.0.0",
"pump": "^3.0.0",
"readable-stream": "^2.3.6",
"reinterval": "^1.1.0",
"split2": "^3.1.0",
"websocket-stream": "^5.1.2",
"xtend": "^4.0.1"
}
},
"mqtt-packet": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz",
"integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==",
"requires": {
"bl": "^4.0.2",
"debug": "^4.1.1",
"process-nextick-args": "^2.0.1"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
},
"next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"requires": {
"wrappy": "1"
}
},
"ordered-read-streams": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz",
"integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==",
"requires": {
"readable-stream": "^2.0.1"
}
},
"path-dirname": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
"integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
},
"picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"requires": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
}
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"pumpify": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz",
"integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==",
"requires": {
"duplexify": "^3.6.0",
"inherits": "^2.0.3",
"pump": "^2.0.0"
},
"dependencies": {
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
}
}
},
"readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"reinterval": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz",
"integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ=="
},
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw=="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
"split2": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
"integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
"requires": {
"readable-stream": "^3.0.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"stream-shift": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
"requires": {
"readable-stream": "~2.3.6",
"xtend": "~4.0.1"
}
},
"through2-filter": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz",
"integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==",
"requires": {
"through2": "~2.0.0",
"xtend": "~4.0.0"
}
},
"to-absolute-glob": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
"integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==",
"requires": {
"is-absolute": "^1.0.0",
"is-negated-glob": "^1.0.0"
}
},
"type": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
},
"ultron": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
},
"unc-path-regex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
"integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg=="
},
"unique-stream": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz",
"integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==",
"requires": {
"json-stable-stringify-without-jsonify": "^1.0.1",
"through2-filter": "^3.0.0"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"vue": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz",
"integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
"requires": {
"@vue/compiler-dom": "3.5.21",
"@vue/compiler-sfc": "3.5.21",
"@vue/runtime-dom": "3.5.21",
"@vue/server-renderer": "3.5.21",
"@vue/shared": "3.5.21"
}
},
"websocket-stream": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.5.2.tgz",
"integrity": "sha512-8z49MKIHbGk3C4HtuHWDtYX8mYej1wWabjthC/RupM9ngeukU4IWoM46dgth1UOS/T4/IqgEdCDJuMe2039OQQ==",
"requires": {
"duplexify": "^3.5.1",
"inherits": "^2.0.1",
"readable-stream": "^2.3.3",
"safe-buffer": "^5.1.2",
"ws": "^3.2.0",
"xtend": "^4.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"ws": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
"requires": {
"async-limiter": "~1.0.0",
"safe-buffer": "~5.1.0",
"ultron": "~1.1.0"
}
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
}
}
}

View File

@ -582,8 +582,10 @@ export default {
// 使itemObj
this.currentGG = this.info.commodity_goods_info_list[this.currentGGIndex];
// promotional_pricesales_price
this.currentGG.sales_price = this.promotional_price;
// promotional_pricesales_price
if (this.promotional_price !== "" && this.promotional_price != null) {
this.currentGG.sales_price = this.promotional_price;
}
//
this.changeImg = this.currentGG.commodity_pic[0];
@ -616,8 +618,10 @@ export default {
//
changeGG(item, index) {
this.currentGG = item;
// promotional_pricesales_price
this.currentGG.sales_price = this.promotional_price;
// promotional_pricesales_price
if (this.promotional_price !== "" && this.promotional_price != null) {
this.currentGG.sales_price = this.promotional_price;
}
this.currentGGIndex = index;
if (this.currentGG.cart_count) {
this.currentNum = this.currentGG.cart_count.count;
@ -778,7 +782,15 @@ export default {
}
},
onLoad(options) {
this.itemObj = JSON.parse(decodeURIComponent(options.item));
// URL
if (options && options.fromChat) {
let stashed
try { stashed = uni.getStorageSync('chatCardItem') } catch (e) { stashed = null }
uni.removeStorageSync('chatCardItem')
this.itemObj = stashed || JSON.parse(decodeURIComponent(options.item))
} else {
this.itemObj = JSON.parse(decodeURIComponent(options.item));
}
const meun = menuButtonInfo();
this.top = meun.top;
this.localHeight = meun.height;

View File

@ -91,6 +91,9 @@ export default {
this.currentCommunity = e
uni.setStorageSync('changeCommData', { id: e.community_id, name: e.name });
uni.setStorageSync('currentCommunityAddr', e.addr);
// room_name111103
const owner = (e.room_owner_list && e.room_owner_list.length) ? e.room_owner_list[0] : null
uni.setStorageSync('currentRoomNo', owner && owner.room_name ? owner.room_name : '');
NavgateTo("1")
},
},

View File

@ -68,9 +68,9 @@ export default {
},
getAvatarUrl(record){
if(record.client_id_one == uni.getStorageSync('openId')){
return record.two.avatar ? picUrl + record.two.avatar : 'https://static.hshuishang.com/defaultTx.png'
return record.two && record.two.avatar ? picUrl + record.two.avatar : 'https://static.hshuishang.com/defaultTx.png'
}else{
return record.one.avatar ? picUrl + record.one.avatar : 'https://static.hshuishang.com/defaultTx.png'
return record.one && record.one.avatar ? picUrl + record.one.avatar : 'https://static.hshuishang.com/defaultTx.png'
}
},
//

View File

@ -33,9 +33,44 @@
}" class="message-item">
<image :src="message.isSelf ? userAvatar : getAvatarUrl(message)" class="message-avatar" mode="aspectFill">
</image>
<view class="message-content">
<!-- 文字 -->
<view v-if="!message.type || message.type === 1" class="message-content">
{{ message.content }}
</view>
<!-- 图片 -->
<view v-else-if="message.type === 2" class="message-media">
<image v-if="message.mediaUrl" :src="message.mediaUrl" class="chat-img" mode="aspectFit"
@tap="previewImage(message.mediaUrl)" />
<view v-else class="media-loading">图片加载中</view>
</view>
<!-- 视频 -->
<view v-else-if="message.type === 3" class="message-media">
<video v-if="message.mediaUrl" :src="message.mediaUrl" class="chat-video" controls></video>
<view v-else class="media-loading">视频加载中</view>
</view>
<!-- 商品/购物车卡片 -->
<view v-else-if="message.type === 4" class="message-card" @tap="openCardLink(message.card)">
<image v-if="message.card.pic && !message.card._picErr" :src="message.card.pic" class="card-pic"
mode="aspectFill" @error="onCardPicError(message)" />
<view v-else class="card-pic card-pic--ph">商品</view>
<view class="card-info">
<view class="card-name">{{ message.card.name }}</view>
<view class="card-price">¥{{ message.card.price }}</view>
<view class="card-tag">商品</view>
</view>
</view>
<!-- 订单卡片 -->
<view v-else-if="message.type === 5" class="message-card" @tap="openCardLink(message.card)">
<image v-if="message.card.pic && !message.card._picErr" :src="message.card.pic" class="card-pic"
mode="aspectFill" @error="onCardPicError(message)" />
<view v-else class="card-pic card-pic--ph">订单</view>
<view class="card-info">
<view class="card-name">订单 {{ message.card.order_no }}</view>
<view class="card-price">¥{{ message.card.amount }}</view>
<view class="card-tag">订单 · {{ message.card.count }}</view>
</view>
</view>
<view v-else class="message-content">{{ message.content }}</view>
</view>
</block>
</scroll-view>
@ -46,18 +81,45 @@
<textarea v-model="inputMessage" :adjust-position="true" auto-height class="message-input" cursor-spacing="10"
enable-keyboard-accessory-view="true" hold-keyboard="true" maxlength="500" placeholder="请输入消息..."
@blur="onInputBlur" @confirm="sendMessage" @focus="onInputFocus" @input="handleInput"></textarea>
<view class="plus-btn" @tap="togglePanel">
<uni-icons color="#666" size="30" type="plusempty"></uni-icons>
</view>
<button :disabled="inputMessage.trim() === ''" class="send-btn" @tap="sendMessage">
发送
</button>
</view>
<!-- 更多功能面板 -->
<view v-if="showPanel" class="more-panel">
<view class="panel-item" @tap="chooseMedia('image')">
<view class="panel-icon">🖼</view>
<text>相册图片</text>
</view>
<view class="panel-item" @tap="chooseMedia('camera')">
<view class="panel-icon">📷</view>
<text>拍摄</text>
</view>
<view class="panel-item" @tap="chooseMedia('video')">
<view class="panel-icon">🎬</view>
<text>视频</text>
</view>
<view class="panel-item" @tap="openGoodsPicker">
<view class="panel-icon">🛒</view>
<text>商品/购物车</text>
</view>
<view class="panel-item" @tap="openOrderPicker">
<view class="panel-icon">📦</view>
<text>订单</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { menuButtonInfo, picUrl, request } from '@/utils'
import { menuButtonInfo, picUrl, request, NavgateTo } from '@/utils'
import mqttTool from '@/utils/mqtt'
import { apiArr } from '../../../api/customerService'
import { uploadOSS, signPrivateView } from '@/utils/uploadOSS'
export default {
data() {
@ -79,6 +141,8 @@ export default {
//
inputMessage: '',
//
showPanel: false,
//
canSend: false,
//
@ -150,6 +214,13 @@ export default {
console.log('用户头像:', this.userAvatar)
},
onShow() {
// picker /
if (!this._onPickCard) {
this._onPickCard = ({ type, card }) => {
this.publishMsg(type, JSON.stringify(card))
}
uni.$on('chat:pickCard', this._onPickCard)
}
},
methods: {
getAvatarUrl(record) {
@ -160,6 +231,7 @@ export default {
const options = {
clientId: this.selfClientId
}
console.log('clientId:', options.clientId)
//
const callbacks = {
@ -192,26 +264,46 @@ export default {
let jsMsg = msg // 使
console.log('收到消息', topic, msg)
if (jsMsg.send_client === this.selfClientId || jsMsg.receive_client === this.selfClientId) {
console.log('接收或发送人是我')
if (jsMsg.send_client === this.chatTarget.openId || jsMsg.receive_client === this.chatTarget.openId) {
console.log('接收或发送人是我的聊天对象')
this.messages.push({
content: jsMsg.content,
time: Date.now(),
isSelf: jsMsg.send_client === this.selfClientId,
isLoading: false
})
console.log('收到我的消息', this.messages)
this.scrollToView = 'msg-' + (this.messages.length - 1)
this.appendMessage(jsMsg, jsMsg.send_client === this.selfClientId)
}
}
})
},
// type/content/ URL
async appendMessage(raw, isSelf) {
const type = Number(raw.type) || 1
const item = {
type,
content: raw.content,
time: Date.now(),
isSelf,
isLoading: false,
mediaUrl: '',
card: null
}
if (type === 4 || type === 5) {
try { item.card = typeof raw.content === 'string' ? JSON.parse(raw.content) : raw.content } catch (e) { item.card = {} }
}
const idx = this.messages.push(item) - 1
this.scrollToView = 'msg-' + idx
// /content object_key
if (type === 2 || type === 3) {
try {
const r = await signPrivateView(raw.content)
this.$set(this.messages[idx], 'mediaUrl', r.url)
} catch (e) {
console.error('签发聊天媒体URL失败', e)
}
}
},
async subscribe() {
if (this.isConnected && this.client) {
this.client.subscribe('contact/message/receive_msg', { qos: 0 }, (err) => {
const topic = 'contact/message/send_msg/' + this.chatTarget.bindId // 沿 contact/message
this.client.subscribe(topic, { qos: 0 }, (err) => {
if (!err) {
console.log('订阅成功', 'contact/message/receive_msg', { qos: 0 })
console.log('订阅成功', topic, { qos: 0 })
this.connectingStatus = ''
} else {
console.log('订阅失败:', err)
@ -347,13 +439,32 @@ export default {
}
//
const formattedMessages = historyMessages.map(msg => ({
content: msg.content,
time: new Date(msg.create_time).getTime(),
times: msg.update_time,
isSelf: msg.send_client === this.selfClientId, //
isLoading: false
})).reverse(); //
const formattedMessages = historyMessages.map(msg => {
const type = Number(msg.type) || 1
const item = {
type,
content: msg.content,
time: new Date(msg.create_time).getTime(),
times: msg.update_time,
isSelf: msg.send_client === this.selfClientId,
isLoading: false,
mediaUrl: '',
card: null
}
if (type === 4 || type === 5) {
try { item.card = typeof msg.content === 'string' ? JSON.parse(msg.content) : msg.content } catch (e) { item.card = {} }
}
return item
}).reverse(); //
// / URL
formattedMessages.forEach(item => {
if (item.type === 2 || item.type === 3) {
signPrivateView(item.content).then(r => {
this.$set(item, 'mediaUrl', r.url)
}).catch(e => console.error('历史媒体签名失败', e))
}
})
console.log('格式化后的历史消息:', formattedMessages)
//
@ -440,7 +551,7 @@ export default {
}
console.log('发送消息', msgData)
this.client.publish(
'contact/message/send_msg', // 使
'contact/message/send_msg/' + this.chatTarget.bindId, //
JSON.stringify(msgData),
{ Qos: 0 },
(err) => {
@ -457,6 +568,107 @@ export default {
this.inputMessage = ''
},
// "+"
togglePanel() {
this.showPanel = !this.showPanel
},
// type: 1 2 3 4 5content /object_key/JSON
publishMsg(type, content) {
if (!this.client || !this.isConnected) {
uni.showToast({ title: '连接断开,请重试', icon: 'none' })
return
}
const msgData = {
bind_id: this.chatTarget.bindId,
send_client: this.selfClientId,
receive_client: this.chatTarget.openId,
type,
content,
receive_read_status: 2
}
this.client.publish('contact/message/send_msg/' + this.chatTarget.bindId, JSON.stringify(msgData), { Qos: 0 }, (err) => {
if (err) console.error('发送消息失败', err)
})
},
// //
chooseMedia(source) {
this.showPanel = false
if (source === 'video') {
uni.chooseVideo({
sourceType: ['album', 'camera'],
maxDuration: 60,
success: (res) => this.uploadAndSend(res.tempFilePath, 'chat_video', 3)
})
} else {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: source === 'camera' ? ['camera'] : ['album'],
success: (res) => {
const fp = res.tempFilePaths && res.tempFilePaths[0]
if (fp) this.uploadAndSend(fp, 'chat_image', 2)
}
})
}
},
// bucket object_key
async uploadAndSend(filePath, scene, type) {
try {
const { objectKey } = await uploadOSS({ filePath, scene, bindId: this.chatTarget.bindId })
this.publishMsg(type, objectKey)
} catch (e) {
console.error('上传失败', e)
uni.showToast({ title: '上传失败,请重试', icon: 'none' })
}
},
//
previewImage(url) {
if (url) uni.previewImage({ urls: [url], current: url })
},
// /
openCardLink(card) {
if (!card) return
// path
if (!card.page && card.path) {
NavgateTo(card.path, { isLogin: false })
return
}
if (!card.page) return
// item URL storage
const item = card.item || {}
try {
uni.setStorageSync('chatCardItem', item)
} catch (e) {
console.error('暂存卡片数据失败', e)
}
NavgateTo(card.page + '?item=' + encodeURIComponent(JSON.stringify(item)) + '&fromChat=1', { isLogin: false })
},
// -> $set
onCardPicError(message) {
if (message && message.card) {
this.$set(message.card, '_picErr', true)
}
},
// /
openGoodsPicker() {
this.showPanel = false
// / publishMsg(4, JSON)
NavgateTo('/packages/customerService/picker/index?type=goods&bindId=' + this.chatTarget.bindId, { isLogin: false })
},
//
openOrderPicker() {
this.showPanel = false
NavgateTo('/packages/customerService/picker/index?type=order&bindId=' + this.chatTarget.bindId, { isLogin: false })
},
//
handleInput() {
this.canSend = this.inputMessage.trim().length > 0
@ -579,6 +791,12 @@ export default {
clearTimeout(this.reconnectFailedTimer)
this.reconnectFailedTimer = null
}
//
if (this._onPickCard) {
uni.$off('chat:pickCard', this._onPickCard)
this._onPickCard = null
}
}
}
</script>
@ -591,4 +809,115 @@ export default {
--header-height: 80px; /* 头部高度 */
--input-height: 80px; /* 输入区域高度 */
}
/* 图片/视频消息 */
.message-media {
max-width: 60%;
}
.chat-img {
width: 320rpx;
height: 320rpx;
border-radius: 12rpx;
background: #f0f0f0;
}
.chat-video {
width: 400rpx;
height: 300rpx;
border-radius: 12rpx;
}
.media-loading {
padding: 30rpx 40rpx;
background: #fff;
border-radius: 12rpx;
color: #999;
font-size: 24rpx;
}
/* 商品/订单卡片 */
.message-card {
display: flex;
width: 520rpx;
background: #fff;
border-radius: 14rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
}
.card-pic {
width: 180rpx;
height: 180rpx;
flex-shrink: 0;
background: #f5f5f5;
}
.card-pic--ph {
display: flex;
align-items: center;
justify-content: center;
color: #bbb;
font-size: 28rpx;
}
.card-info {
flex: 1;
padding: 18rpx 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
}
.card-name {
font-size: 28rpx;
color: #222;
line-height: 38rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-price {
font-size: 32rpx;
color: #FF370B;
font-weight: 600;
}
.card-tag {
align-self: flex-start;
font-size: 20rpx;
color: #999;
border: 1rpx solid #eee;
border-radius: 6rpx;
padding: 0 10rpx;
}
/* "+" 按钮与更多面板 */
.plus-btn {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
}
.more-panel {
display: flex;
flex-wrap: wrap;
padding: 24rpx 12rpx;
background: #f7f7f7;
}
.panel-item {
width: 20%;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
font-size: 22rpx;
color: #666;
margin-bottom: 16rpx;
}
.panel-icon {
width: 96rpx;
height: 96rpx;
background: #fff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 44rpx;
}
</style>

View File

@ -0,0 +1,165 @@
<template>
<view class="picker-page">
<!-- 订单普通/团购 两个 tab -->
<view v-if="mode === 'order'" class="tabs">
<view class="tab" :class="{ active: orderTab === 'normal' }" @tap="switchTab('normal')">普通订单</view>
<view class="tab" :class="{ active: orderTab === 'group' }" @tap="switchTab('group')">团购订单</view>
</view>
<scroll-view class="list" scroll-y>
<!-- 订单列表 -->
<block v-if="mode === 'order'">
<view v-for="(item, idx) in orderList" :key="idx" class="order-card" @tap="pickOrder(item)">
<view class="order-head">
<text class="order-no">订单号{{ item.order_no }}</text>
</view>
<view class="order-goods">
<image v-for="(g, gi) in (item.commodity_order_item_list || []).slice(0, 4)" :key="gi"
:src="g.commodity_pic" class="goods-thumb" mode="aspectFill" />
</view>
<view class="order-foot">
<text>{{ item.total_count }}</text>
<text class="amount">¥{{ item.total_amount }}</text>
<view class="send-btn">发送</view>
</view>
</view>
<view v-if="orderList.length === 0" class="empty">暂无订单</view>
</block>
<!-- 购物车列表 -->
<block v-else>
<view v-for="(g, idx) in cartGoods" :key="idx" class="goods-row" @tap="pickGoods(g)">
<image :src="g.pic" class="goods-thumb-lg" mode="aspectFill" />
<view class="goods-info">
<view class="goods-name">{{ g.name }}</view>
<view class="goods-price">¥{{ g.price }}</view>
</view>
<view class="send-btn">发送</view>
</view>
<view v-if="cartGoods.length === 0" class="empty">购物车为空</view>
</block>
</scroll-view>
</view>
</template>
<script>
import { request, picUrl } from '@/utils'
import { apiArr as shopApi } from '@/api/shop'
import { apiArr as afterSaleApi } from '@/api/afterSale'
export default {
data() {
return {
mode: 'order', // order | goods
bindId: 0,
orderTab: 'normal', // normal | group
orderList: [],
cartGoods: []
}
},
onLoad(options) {
this.mode = options.type === 'goods' ? 'goods' : 'order'
this.bindId = Number(options.bindId) || 0
uni.setNavigationBarTitle({ title: this.mode === 'goods' ? '选择购物车商品' : '选择订单' })
if (this.mode === 'goods') {
this.loadCart()
} else {
this.loadOrders()
}
},
methods: {
switchTab(t) {
this.orderTab = t
this.loadOrders()
},
// order_cate 1 2 tab
loadOrders() {
request(afterSaleApi.orderList, 'POST', {
user_id: uni.getStorageSync('userId')
}).then(res => {
const list = res.order_list || []
list.forEach(item => {
(item.commodity_order_item_list || []).forEach(g => {
g.commodity_pic = (g.commodity_pic && g.commodity_pic.startsWith('http')) ? g.commodity_pic : picUrl + g.commodity_pic
})
})
if (this.orderTab === 'group') {
this.orderList = list.filter(i => i.order_cate == 2)
} else {
this.orderList = list.filter(i => i.order_cate == 1 || i.order_cate == 4)
}
})
},
//
loadCart() {
request(shopApi.getCar, 'POST', { is_group_buy: '' }).then(res => {
const groups = [].concat(res.same_day_cart_list || [], res.normal_cart_list || [])
const goods = []
groups.forEach(sup => {
(sup.commodity_cart_and_goods_model || []).forEach(c => {
const gi = c.commodity_goods_info || {}
goods.push({
goods_id: c.goods_id,
commodity_id: c.commodity_id,
name: gi.commodity_name || gi.goods_name || '商品',
pic: (gi.commodity_pic && gi.commodity_pic.startsWith('http')) ? gi.commodity_pic : picUrl + (gi.commodity_pic || ''),
price: gi.sales_price || c.price || 0
})
})
})
this.cartGoods = goods
})
},
// -> type=5
pickOrder(item) {
const firstPic = (item.commodity_order_item_list && item.commodity_order_item_list[0] && item.commodity_order_item_list[0].commodity_pic) || ''
const card = {
order_id: item.id,
order_no: item.order_no,
amount: item.total_amount,
count: item.total_count,
pic: firstPic,
page: '/packages/myOrders/orderDetails/index', // item storage URL
item //
}
uni.$emit('chat:pickCard', { type: 5, card })
uni.navigateBack()
},
// -> type=4
pickGoods(g) {
const card = {
goods_id: g.goods_id,
commodity_id: g.commodity_id,
name: g.name,
price: g.price,
pic: g.pic,
page: '/packages/advertising/goodsDetail/index',
item: { commodity_id: g.commodity_id, id: g.goods_id, promotional_price: g.price } // goodsDetail commodity_id id promotional_price
}
uni.$emit('chat:pickCard', { type: 4, card })
uni.navigateBack()
}
}
}
</script>
<style scoped>
.picker-page { display: flex; flex-direction: column; height: 100vh; background: #f5f5f5; }
.tabs { display: flex; background: #fff; }
.tab { flex: 1; text-align: center; padding: 28rpx 0; font-size: 28rpx; color: #666; }
.tab.active { color: #FF370B; font-weight: 600; border-bottom: 4rpx solid #FF370B; }
.list { flex: 1; padding: 20rpx; box-sizing: border-box; }
.order-card { background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 20rpx; }
.order-head { font-size: 24rpx; color: #999; margin-bottom: 14rpx; }
.order-goods { display: flex; gap: 12rpx; }
.goods-thumb { width: 120rpx; height: 120rpx; border-radius: 8rpx; background: #f0f0f0; }
.order-foot { display: flex; align-items: center; justify-content: flex-end; gap: 20rpx; margin-top: 16rpx; }
.order-foot .amount { color: #FF370B; font-weight: 600; }
.goods-row { display: flex; align-items: center; background: #fff; border-radius: 12rpx; padding: 16rpx; margin-bottom: 16rpx; }
.goods-thumb-lg { width: 140rpx; height: 140rpx; border-radius: 8rpx; background: #f0f0f0; flex-shrink: 0; }
.goods-info { flex: 1; margin-left: 20rpx; min-width: 0; }
.goods-name { font-size: 28rpx; color: #222; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.goods-price { font-size: 30rpx; color: #FF370B; font-weight: 600; margin-top: 12rpx; }
.send-btn { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; font-size: 24rpx; padding: 10rpx 28rpx; border-radius: 30rpx; flex-shrink: 0; }
.empty { text-align: center; color: #999; font-size: 26rpx; padding: 80rpx 0; }
</style>

View File

@ -0,0 +1,79 @@
<template>
<view class="hcd-page" v-if="info.id">
<view class="hcd-card">
<view class="hcd-status" :class="'st' + info.status">{{ statusText(info.status) }}</view>
<view class="hcd-name">{{ info.contract_name }}</view>
<view class="hcd-no">合同编号{{ info.contract_no }}</view>
</view>
<view class="hcd-block">
<view class="hcd-item"><text class="hcd-label">月薪</text><text class="hcd-val amount">¥{{ info.month_amount }}</text></view>
<view class="hcd-item"><text class="hcd-label">账单日</text><text class="hcd-val">每月 {{ info.pay_day }} </text></view>
<view class="hcd-item"><text class="hcd-label">结算方式</text><text class="hcd-val">{{ info.settle_mode === 1 ? '平台月账单' : '线下自付' }}</text></view>
<view class="hcd-item"><text class="hcd-label">合同开始</text><text class="hcd-val">{{ fmtDate(info.start_date) }}</text></view>
<view class="hcd-item"><text class="hcd-label">合同结束</text><text class="hcd-val">{{ fmtDate(info.end_date) }}</text></view>
<view class="hcd-item"><text class="hcd-label">联系人</text><text class="hcd-val">{{ info.contact_name || '-' }}</text></view>
<view class="hcd-item"><text class="hcd-label">联系电话</text><text class="hcd-val">{{ info.contact_phone || '-' }}</text></view>
<view class="hcd-item"><text class="hcd-label">服务地址</text><text class="hcd-val">{{ info.service_address || '-' }}</text></view>
<view class="hcd-item" v-if="info.remark"><text class="hcd-label">备注</text><text class="hcd-val">{{ info.remark }}</text></view>
</view>
<view class="hcd-block" v-if="info.file_url">
<view class="hcd-file" @tap="openFile">查看已签合同</view>
</view>
</view>
</template>
<script>
import { request, picUrl } from '@/utils'
import { apiArr } from '@/api/homeService'
export default {
data() {
return { picUrl, id: 0, info: {} }
},
onLoad(options) {
this.id = Number(options.id)
this.loadInfo()
},
methods: {
loadInfo() {
const userId = uni.getStorageSync('userId')
request(apiArr.contractInfo, 'POST', { id: this.id, user_id: userId }, {}, false).then(res => {
this.info = res || {}
})
},
statusText(s) {
const map = { 1: '待签署', 2: '生效中', 3: '已到期', 4: '已解除', 5: '线下结算' }
return map[s] || ''
},
fmtDate(v) {
return v ? String(v).slice(0, 10) : '-'
},
openFile() {
const url = this.info.file_url.startsWith('http') ? this.info.file_url : this.picUrl + this.info.file_url
uni.downloadFile({
url,
success: (d) => {
uni.openDocument({ filePath: d.tempFilePath, showMenu: true })
},
fail: () => uni.showToast({ title: '打开失败', icon: 'none' })
})
}
}
}
</script>
<style scoped>
.hcd-page { background: #f5f5f5; min-height: 100vh; padding: 20rpx; }
.hcd-card { background: #fff; border-radius: 16rpx; padding: 28rpx; margin-bottom: 20rpx; }
.hcd-status { font-size: 26rpx; color: #FF370B; margin-bottom: 12rpx; }
.hcd-status.st2 { color: #07c160; }
.hcd-status.st3, .hcd-status.st4 { color: #999; }
.hcd-name { font-size: 34rpx; color: #222; font-weight: 600; }
.hcd-no { font-size: 26rpx; color: #888; margin-top: 10rpx; }
.hcd-block { background: #fff; border-radius: 16rpx; padding: 8rpx 28rpx; margin-bottom: 20rpx; }
.hcd-item { display: flex; justify-content: space-between; padding: 22rpx 0; border-bottom: 1rpx solid #f2f2f2; }
.hcd-label { font-size: 28rpx; color: #888; }
.hcd-val { font-size: 28rpx; color: #222; max-width: 460rpx; text-align: right; }
.hcd-val.amount { color: #FF370B; font-weight: 600; }
.hcd-file { text-align: center; color: #2b6cff; font-size: 28rpx; padding: 24rpx 0; }
</style>

View File

@ -0,0 +1,152 @@
<template>
<view class="hsd-page">
<swiper class="hsd-swiper" autoplay circular v-if="carousel.length">
<swiper-item v-for="(p, i) in carousel" :key="i">
<image :src="p" mode="aspectFill" class="hsd-swiper-img" />
</swiper-item>
</swiper>
<image v-else :src="picUrl + (info.cover_pic || '')" class="hsd-swiper-img" mode="aspectFill" />
<view class="hsd-main">
<view class="hsd-name">{{ info.service_name }}</view>
<view class="hsd-pricedesc">{{ info.price_desc }}</view>
</view>
<!-- SKU 选择 -->
<view class="hsd-block" v-if="skuList.length">
<view class="hsd-block-tit">选择规格</view>
<view class="hsd-sku-list">
<view class="hsd-sku" :class="{ active: currentSku && currentSku.id === sku.id }"
v-for="sku in skuList" :key="sku.id" @tap="selectSku(sku)">
<view class="hsd-sku-name">{{ sku.sku_name }}</view>
<view class="hsd-sku-price" v-if="isHousekeeping">月薪 ¥{{ sku.month_salary }}</view>
<view class="hsd-sku-price" v-else>定金 ¥{{ sku.deposit }}</view>
</view>
</view>
</view>
<!-- 师傅选择 -->
<view class="hsd-block" v-if="workerList.length">
<view class="hsd-block-tit">选择师傅</view>
<view class="hsd-worker-list">
<view class="hsd-worker" :class="{ active: currentWorker && currentWorker.employee_id === w.employee_id }"
v-for="w in workerList" :key="w.employee_id" @tap="selectWorker(w)">
<image class="hsd-worker-img" :src="w.employee_image || defaultAvatar" mode="aspectFill" />
<view class="hsd-worker-name">{{ w.employee_name }}</view>
<view class="hsd-worker-exp">{{ w.expertise }}</view>
</view>
</view>
</view>
<!-- 详情图 -->
<view class="hsd-block" v-if="detailPics.length">
<view class="hsd-block-tit">服务详情</view>
<image v-for="(p, i) in detailPics" :key="i" :src="p" mode="widthFix" class="hsd-detail-img" />
</view>
<view class="hsd-bottom">
<template v-if="isHousekeeping">
<view class="hsd-bottom-price">月薪 <text>¥{{ currentSku ? currentSku.month_salary : '--' }}</text></view>
<view class="hsd-bottom-btn" @tap="goConsult">咨询预约</view>
</template>
<template v-else>
<view class="hsd-bottom-price">定金 <text>¥{{ currentSku ? currentSku.deposit : '--' }}</text></view>
<view class="hsd-bottom-btn" @tap="goOrder">立即预约</view>
</template>
</view>
</view>
</template>
<script>
import { request, picUrl, NavgateTo } from '@/utils'
import { apiArr } from '@/api/homeService'
export default {
data() {
return {
picUrl,
defaultAvatar: 'https://static.hshuishang.com/defaultTx.png',
id: 0,
info: {},
carousel: [],
detailPics: [],
skuList: [],
workerList: [],
currentSku: null,
currentWorker: null
}
},
onLoad(options) {
this.id = Number(options.id)
this.loadInfo()
},
computed: {
// ( service_type=2)
isHousekeeping() {
return Number(this.info.service_type) === 2
}
},
methods: {
loadInfo() {
request(apiArr.serviceInfo, 'POST', { id: this.id }, {}, false).then(res => {
this.info = res || {}
this.carousel = (res.carousel ? res.carousel.split(',') : []).filter(Boolean).map(p => p.startsWith('http') ? p : this.picUrl + p)
this.detailPics = (res.detail_pic ? res.detail_pic.split(',') : []).filter(Boolean).map(p => p.startsWith('http') ? p : this.picUrl + p)
this.skuList = res.sku_list || []
this.workerList = res.worker_list || []
if (this.skuList.length) this.currentSku = this.skuList[0]
if (this.workerList.length) this.currentWorker = this.workerList[0]
})
},
selectSku(sku) { this.currentSku = sku },
selectWorker(w) { this.currentWorker = w },
goOrder() {
if (!this.currentSku) { uni.showToast({ title: '请选择规格', icon: 'none' }); return }
if (!this.currentWorker) { uni.showToast({ title: '请选择师傅', icon: 'none' }); return }
const params = {
service_info_id: this.id,
sku_id: this.currentSku.id,
employee_id: this.currentWorker.employee_id,
service_name: this.info.service_name,
deposit: this.currentSku.deposit
}
NavgateTo('/packages/homeService/order/index?params=' + encodeURIComponent(JSON.stringify(params)))
},
//
goConsult() {
const supplierId = this.info.supplier_id
if (!supplierId) { uni.showToast({ title: '商家信息缺失', icon: 'none' }); return }
// merchantInfo.id
const mi = uni.getStorageSync('merchantInfo') || {}
mi.id = supplierId
uni.setStorageSync('merchantInfo', mi)
NavgateTo('/packages/customerService/changeService/index')
}
}
}
</script>
<style scoped>
.hsd-page { background: #f5f5f5; min-height: 100vh; padding-bottom: 130rpx; }
.hsd-swiper, .hsd-swiper-img { width: 100%; height: 520rpx; }
.hsd-main { background: #fff; padding: 24rpx; }
.hsd-name { font-size: 36rpx; font-weight: 600; color: #222; }
.hsd-pricedesc { font-size: 26rpx; color: #FF370B; margin-top: 14rpx; }
.hsd-block { background: #fff; margin-top: 16rpx; padding: 24rpx; }
.hsd-block-tit { font-size: 30rpx; font-weight: 600; color: #222; margin-bottom: 18rpx; }
.hsd-sku-list, .hsd-worker-list { display: flex; flex-wrap: wrap; gap: 18rpx; }
.hsd-sku { border: 1rpx solid #eee; border-radius: 10rpx; padding: 16rpx 24rpx; }
.hsd-sku.active { border-color: #FF370B; background: #FFF1ED; }
.hsd-sku-name { font-size: 28rpx; color: #222; }
.hsd-sku-price { font-size: 24rpx; color: #FF370B; margin-top: 8rpx; }
.hsd-worker { width: 160rpx; display: flex; flex-direction: column; align-items: center; border: 1rpx solid #eee; border-radius: 10rpx; padding: 16rpx 8rpx; }
.hsd-worker.active { border-color: #FF370B; background: #FFF1ED; }
.hsd-worker-img { width: 100rpx; height: 100rpx; border-radius: 50%; background: #f0f0f0; }
.hsd-worker-name { font-size: 26rpx; color: #222; margin-top: 10rpx; }
.hsd-worker-exp { font-size: 20rpx; color: #999; margin-top: 4rpx; }
.hsd-detail-img { width: 100%; display: block; }
.hsd-bottom { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; align-items: center; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.04); }
.hsd-bottom-price { flex: 1; font-size: 26rpx; color: #333; }
.hsd-bottom-price text { color: #FF370B; font-size: 36rpx; font-weight: 600; }
.hsd-bottom-btn { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; font-size: 30rpx; padding: 20rpx 60rpx; border-radius: 44rpx; }
</style>

View File

@ -0,0 +1,124 @@
<template>
<view class="hs-page">
<!-- 到家/家政 Tab -->
<view class="hs-tabs">
<view class="hs-tab" :class="{ active: serviceType === 1 }" @tap="switchType(1)">到家服务</view>
<view class="hs-tab" :class="{ active: serviceType === 2 }" @tap="switchType(2)">家政服务</view>
</view>
<view class="hs-body">
<!-- 左侧分类 -->
<scroll-view class="hs-cate" scroll-y>
<view class="hs-cate-item" :class="{ active: currentCate === 0 }" @tap="selectCate(0)">全部</view>
<view class="hs-cate-item" :class="{ active: currentCate === item.id }"
v-for="item in categoryList" :key="item.id" @tap="selectCate(item.id)">
{{ item.category_name }}
</view>
</scroll-view>
<!-- 右侧服务列表 -->
<scroll-view class="hs-list" scroll-y @scrolltolower="loadMore">
<view class="hs-card" v-for="item in serviceList" :key="item.id" @tap="toDetail(item)">
<image class="hs-card-pic" :src="picUrl + item.cover_pic" mode="aspectFill" />
<view class="hs-card-info">
<view class="hs-card-name">{{ item.service_name }}</view>
<view class="hs-card-desc">{{ item.price_desc }}</view>
<view class="hs-card-foot">
<text class="hs-card-tag">{{ serviceType === 1 ? '到家' : '家政' }}</text>
<text class="hs-card-btn">查看</text>
</view>
</view>
</view>
<view v-if="serviceList.length === 0" class="hs-empty">暂无服务</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { request, picUrl, NavgateTo } from '@/utils'
import { apiArr } from '@/api/homeService'
export default {
data() {
return {
picUrl,
serviceType: 1,
categoryList: [],
currentCate: 0,
serviceList: [],
pageNum: 1,
pageSize: 10,
hasMore: true
}
},
onLoad() {
this.loadCategory()
this.loadList(true)
},
methods: {
switchType(t) {
if (this.serviceType === t) return
this.serviceType = t
this.currentCate = 0
this.loadCategory()
this.loadList(true)
},
selectCate(id) {
this.currentCate = id
this.loadList(true)
},
loadCategory() {
request(apiArr.categoryList, 'POST', { service_type: this.serviceType }, {}, false).then(res => {
this.categoryList = res.rows || []
})
},
loadList(reset) {
if (reset) {
this.pageNum = 1
this.hasMore = true
this.serviceList = []
}
if (!this.hasMore) return
request(apiArr.serviceList, 'POST', {
service_type: this.serviceType,
category_id: this.currentCate,
page_num: this.pageNum,
page_size: this.pageSize
}, {}, false).then(res => {
const rows = res.rows || []
this.serviceList = this.serviceList.concat(rows)
this.hasMore = rows.length === this.pageSize
this.pageNum++
})
},
loadMore() {
this.loadList(false)
},
toDetail(item) {
NavgateTo('/packages/homeService/detail/index?id=' + item.id)
}
}
}
</script>
<style scoped>
.hs-page { display: flex; flex-direction: column; height: 100vh; background: #f5f5f5; }
.hs-tabs { display: flex; background: #fff; }
.hs-tab { flex: 1; text-align: center; padding: 26rpx 0; font-size: 28rpx; color: #666; }
.hs-tab.active { color: #FF370B; font-weight: 600; border-bottom: 4rpx solid #FF370B; }
.hs-body { flex: 1; display: flex; overflow: hidden; }
.hs-cate { width: 180rpx; background: #fafafa; height: 100%; }
.hs-cate-item { padding: 28rpx 16rpx; font-size: 26rpx; color: #555; text-align: center; }
.hs-cate-item.active { background: #fff; color: #FF370B; font-weight: 600; }
.hs-list { flex: 1; padding: 20rpx; box-sizing: border-box; }
.hs-card { display: flex; background: #fff; border-radius: 12rpx; padding: 16rpx; margin-bottom: 16rpx; }
.hs-card-pic { width: 200rpx; height: 200rpx; border-radius: 8rpx; flex-shrink: 0; background: #f0f0f0; }
.hs-card-info { flex: 1; margin-left: 18rpx; display: flex; flex-direction: column; min-width: 0; }
.hs-card-name { font-size: 30rpx; color: #222; font-weight: 600; }
.hs-card-desc { font-size: 24rpx; color: #999; margin-top: 12rpx; flex: 1; }
.hs-card-foot { display: flex; align-items: center; justify-content: space-between; }
.hs-card-tag { font-size: 20rpx; color: #FF370B; border: 1rpx solid #FFD9CD; border-radius: 6rpx; padding: 2rpx 10rpx; }
.hs-card-btn { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; font-size: 24rpx; padding: 8rpx 28rpx; border-radius: 30rpx; }
.hs-empty { text-align: center; color: #999; font-size: 26rpx; padding: 80rpx 0; }
</style>

View File

@ -0,0 +1,66 @@
<template>
<view class="hsc-page">
<view class="hsc-list">
<view class="hsc-card" v-for="c in contracts" :key="c.id" @tap="toDetail(c)">
<view class="hsc-head">
<text class="hsc-no">{{ c.contract_no }}</text>
<text class="hsc-status" :class="'st' + c.status">{{ statusText(c.status) }}</text>
</view>
<view class="hsc-body">
<view class="hsc-name">{{ c.contract_name }}</view>
<view class="hsc-row">月薪<text class="hsc-amount">¥{{ c.month_amount }}</text></view>
<view class="hsc-row">结算{{ c.settle_mode === 1 ? '平台月账单' : '线下自付' }}</view>
<view class="hsc-row">合同期{{ fmtDate(c.start_date) }} ~ {{ fmtDate(c.end_date) }}</view>
</view>
</view>
<view v-if="contracts.length === 0" class="hsc-empty">暂无家政合同</view>
</view>
</view>
</template>
<script>
import { request, NavgateTo } from '@/utils'
import { apiArr } from '@/api/homeService'
export default {
data() {
return { contracts: [] }
},
onShow() {
this.loadContracts()
},
methods: {
loadContracts() {
const userId = uni.getStorageSync('userId')
request(apiArr.contractList, 'POST', { user_id: userId, page_num: 1, page_size: 50 }, {}, false).then(res => {
this.contracts = res.rows || []
})
},
statusText(s) {
const map = { 1: '待签署', 2: '生效中', 3: '已到期', 4: '已解除', 5: '线下结算' }
return map[s] || ''
},
fmtDate(v) {
return v ? String(v).slice(0, 10) : ''
},
toDetail(c) {
NavgateTo('/packages/homeService/contractDetail/index?id=' + c.id)
}
}
}
</script>
<style scoped>
.hsc-page { background: #f5f5f5; min-height: 100vh; padding: 20rpx; }
.hsc-card { background: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 20rpx; }
.hsc-head { display: flex; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #f2f2f2; padding-bottom: 16rpx; }
.hsc-no { font-size: 26rpx; color: #888; }
.hsc-status { font-size: 26rpx; color: #FF370B; }
.hsc-status.st2 { color: #07c160; }
.hsc-status.st3, .hsc-status.st4 { color: #999; }
.hsc-body { padding-top: 16rpx; }
.hsc-name { font-size: 30rpx; color: #222; font-weight: 600; margin-bottom: 12rpx; }
.hsc-row { font-size: 26rpx; color: #555; margin-top: 8rpx; }
.hsc-amount { color: #FF370B; font-weight: 600; }
.hsc-empty { text-align: center; color: #999; font-size: 28rpx; padding-top: 120rpx; }
</style>

View File

@ -0,0 +1,95 @@
<template>
<view class="hmb-page">
<view class="hmb-list">
<view class="hmb-card" v-for="b in bills" :key="b.id">
<view class="hmb-head">
<text class="hmb-no">{{ b.bill_no }}</text>
<text class="hmb-status" :class="b.pay_status === 1 ? 'status-warn' : b.pay_status === 2 ? 'status-ok' : ''">{{ statusText(b.pay_status) }}</text>
</view>
<view class="hmb-body" @tap="toContract(b)">
<image class="hmb-pic" :src="picUrl + (b.service_pic || '')" mode="aspectFill" />
<view class="hmb-info">
<view class="hmb-name">{{ b.service_name }}</view>
<view class="hmb-month">{{ b.bill_month }} 月度账单</view>
<view class="hmb-amount">¥{{ b.amount }}</view>
</view>
</view>
<view class="hmb-foot">
<view class="hmb-link" @tap.stop="toContract(b)">查看合同 </view>
<view class="hmb-pay" v-if="b.pay_status === 1" @tap.stop="payBill(b)">去支付</view>
</view>
</view>
<view v-if="bills.length === 0" class="hmb-empty">暂无待支付月账单</view>
</view>
</view>
</template>
<script>
import { request, picUrl, NavgateTo } from '@/utils'
import { apiArr } from '@/api/homeService'
export default {
data() {
return { picUrl, bills: [] }
},
onShow() {
this.loadBills()
},
methods: {
loadBills() {
const userId = uni.getStorageSync('userId')
request(apiArr.userMonthlyBillList, 'POST', { user_id: userId, page_num: 1, page_size: 50 }, {}, false).then(res => {
this.bills = res.rows || []
})
},
statusText(payStatus) {
return payStatus === 1 ? '待支付' : payStatus === 2 ? '已支付' : payStatus === 3 ? '已退款' : ''
},
toContract(b) {
NavgateTo('/packages/homeService/contractDetail/index?id=' + b.contract_id)
},
payBill(b) {
const userId = uni.getStorageSync('userId')
request(apiArr.userMonthlyBillPreOrder, 'POST', { bill_id: b.id, user_id: userId, trans_type: '71' }).then(pay => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: pay.timeStamp,
nonceStr: pay.nonceStr,
package: pay.package,
signType: pay.signType || 'RSA',
paySign: pay.paySign,
success: () => {
request(apiArr.userMonthlyBillTradeQuery, 'POST', { bill_id: b.id }, {}, false).finally(() => {
uni.showToast({ title: '支付成功', icon: 'none' })
this.loadBills()
})
},
fail: () => { uni.showToast({ title: '支付已取消', icon: 'none' }) }
})
}).catch(err => {
uni.showToast({ title: err.errMsg || '发起支付失败', icon: 'none' })
})
}
}
}
</script>
<style scoped>
.hmb-page { background: #f5f5f5; min-height: 100vh; padding: 20rpx; box-sizing: border-box; }
.hmb-list {}
.hmb-card { background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 18rpx; border-left: 6rpx solid #FF370B; }
.hmb-head { display: flex; justify-content: space-between; align-items: center; font-size: 24rpx; color: #999; margin-bottom: 16rpx; }
.hmb-status { font-size: 26rpx; }
.status-warn { color: #FF9800; }
.status-ok { color: #4CAF50; }
.hmb-body { display: flex; }
.hmb-pic { width: 150rpx; height: 150rpx; border-radius: 8rpx; background: #f0f0f0; flex-shrink: 0; }
.hmb-info { flex: 1; margin-left: 18rpx; display: flex; flex-direction: column; }
.hmb-name { font-size: 30rpx; color: #222; font-weight: 600; }
.hmb-month { font-size: 24rpx; color: #666; margin-top: 8rpx; }
.hmb-amount { font-size: 32rpx; color: #FF370B; font-weight: 600; margin-top: auto; }
.hmb-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 16rpx; }
.hmb-link { font-size: 24rpx; color: #666; }
.hmb-pay { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; font-size: 26rpx; padding: 12rpx 40rpx; border-radius: 40rpx; margin-left: auto; }
.hmb-empty { text-align: center; color: #999; font-size: 26rpx; padding: 80rpx 0; }
</style>

View File

@ -0,0 +1,108 @@
<template>
<view class="hsm-page">
<view class="hsm-list">
<view class="hsm-card" v-for="o in orders" :key="o.id">
<view class="hsm-head">
<text class="hsm-no">{{ o.order_no }}</text>
<text class="hsm-status">{{ statusText(o) }}</text>
</view>
<view class="hsm-body" @tap="toDetail(o)">
<image class="hsm-pic" :src="picUrl + (o.service_pic || '')" mode="aspectFill" />
<view class="hsm-info">
<view class="hsm-name">{{ o.service_name }}</view>
<view class="hsm-kind">{{ kindText(o.order_kind) }}</view>
<view class="hsm-amount">¥{{ o.amount }}</view>
</view>
</view>
<view class="hsm-foot" v-if="o.pay_status === 1 && o.status !== 5 && o.status !== 4">
<view class="hsm-cancel" @tap="cancelOrder(o)">取消订单</view>
<view class="hsm-pay" @tap="payOrder(o)">去支付</view>
</view>
</view>
<view v-if="orders.length === 0" class="hsm-empty">暂无服务订单</view>
</view>
</view>
</template>
<script>
import { request, picUrl, NavgateTo } from '@/utils'
import { apiArr } from '@/api/homeService'
export default {
data() {
return { picUrl, orders: [] }
},
onShow() {
this.loadOrders()
},
methods: {
loadOrders() {
const userId = uni.getStorageSync('userId')
request(apiArr.orderList, 'POST', { user_id: userId, page_num: 1, page_size: 50 }, {}, false).then(res => {
this.orders = res.rows || []
})
},
kindText(k) {
return k === 1 ? '定金/上门费' : k === 2 ? '补差/尾款' : k === 3 ? '月账单' : ''
},
statusText(o) {
if (o.status === 5) return '已取消'
if (o.status === 4) return '已完成'
if (o.pay_status === 1) return '待支付'
const map = { 1: '待上门', 2: '服务中', 3: '待补款' }
return map[o.status] || ''
},
toDetail(o) {
NavgateTo('/packages/homeService/orderDetail/index?id=' + o.id)
},
cancelOrder(o) {
uni.showModal({
title: '提示',
content: '确定取消该订单吗?',
success: (r) => {
if (!r.confirm) return
const userId = uni.getStorageSync('userId')
request(apiArr.orderCancel, 'POST', { id: o.id, user_id: Number(userId) || 0 }).then(() => {
uni.showToast({ title: '已取消', icon: 'none' })
this.loadOrders()
})
}
})
},
payOrder(o) {
const userId = uni.getStorageSync('userId')
request(apiArr.preorder, 'POST', { order_id: o.id, user_id: userId, trans_type: '71' }).then(pay => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: pay.timeStamp,
nonceStr: pay.nonceStr,
package: pay.package,
signType: pay.signType || 'RSA',
paySign: pay.paySign,
success: () => {
request(apiArr.tradeQuery, 'POST', { order_id: o.id }, {}, false).finally(() => this.loadOrders())
},
fail: () => { uni.showToast({ title: '支付已取消', icon: 'none' }) }
})
})
}
}
}
</script>
<style scoped>
.hsm-page { background: #f5f5f5; min-height: 100vh; padding: 20rpx; box-sizing: border-box; }
.hsm-card { background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 18rpx; }
.hsm-head { display: flex; justify-content: space-between; align-items: center; font-size: 24rpx; color: #999; margin-bottom: 16rpx; }
.hsm-status { color: #FF370B; }
.hsm-body { display: flex; }
.hsm-pic { width: 150rpx; height: 150rpx; border-radius: 8rpx; background: #f0f0f0; flex-shrink: 0; }
.hsm-info { flex: 1; margin-left: 18rpx; display: flex; flex-direction: column; }
.hsm-name { font-size: 30rpx; color: #222; font-weight: 600; }
.hsm-kind { font-size: 24rpx; color: #999; margin-top: 10rpx; flex: 1; }
.hsm-amount { font-size: 32rpx; color: #FF370B; font-weight: 600; }
.hsm-foot { display: flex; justify-content: flex-end; align-items: center; margin-top: 16rpx; }
.hsm-cancel { border: 1rpx solid #ccc; color: #666; font-size: 26rpx; padding: 11rpx 36rpx; border-radius: 40rpx; margin-right: 20rpx; }
.hsm-pay { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; font-size: 26rpx; padding: 12rpx 40rpx; border-radius: 40rpx; }
.hsm-empty { text-align: center; color: #999; font-size: 26rpx; padding: 80rpx 0; }
</style>

View File

@ -0,0 +1,221 @@
<template>
<view class="hso-page">
<view class="hso-card">
<view class="hso-svc">{{ params.service_name }}</view>
<view class="hso-deposit">定金 ¥{{ params.deposit }}</view>
</view>
<view class="hso-form">
<view class="hso-row">
<text class="hso-label">联系人</text>
<input class="hso-input" v-model="contactName" placeholder="请输入联系人" />
</view>
<view class="hso-row">
<text class="hso-label">联系电话</text>
<input class="hso-input" type="number" v-model="contactPhone" placeholder="请输入联系电话" maxlength="11" />
</view>
<view class="hso-row">
<text class="hso-label">小区地址</text>
<input class="hso-input" v-model="serviceAddress" placeholder="请选择或输入小区地址" />
<text class="hso-map-btn" @tap="chooseLocation">地图选点</text>
</view>
<view class="hso-row">
<text class="hso-label">门牌号</text>
<input class="hso-input" v-model="houseNumber" placeholder="如1栋1单元103室" />
</view>
<view class="hso-row" @tap="showDate = true">
<text class="hso-label">上门时间</text>
<text class="hso-input" :class="{ ph: !serviceTime }">{{ serviceTime || '请选择上门时间' }}</text>
</view>
<view class="hso-row hso-row--col">
<text class="hso-label">备注</text>
<textarea class="hso-textarea" v-model="remark" placeholder="选填" />
</view>
</view>
<view class="hso-bottom">
<view class="hso-bottom-price">定金 <text>¥{{ params.deposit }}</text></view>
<view class="hso-bottom-btn" @tap="submit">提交并支付</view>
</view>
<u-datetime-picker :show="showDate" :value="dateValue" mode="datetime" @confirm="onDate" @cancel="showDate = false" @close="showDate = false" />
</view>
</template>
<script>
import { request, NavgateTo, isPhone } from '@/utils'
import { apiArr } from '@/api/homeService'
import { apiArr as commApi } from '@/api/community'
export default {
data() {
return {
params: {},
contactName: '',
contactPhone: '',
serviceAddress: '',
houseNumber: '',
serviceTime: '',
dateValue: Date.now(),
remark: '',
showDate: false,
submitting: false
}
},
onLoad(options) {
if (options.params) {
try { this.params = JSON.parse(decodeURIComponent(options.params)) } catch (e) { this.params = {} }
}
//
this.fillFromStorage()
this.loadBoundHouse()
// 3
this.initDefaultTime()
},
methods: {
// +
fillFromStorage() {
const addr = uni.getStorageSync('currentCommunityAddr')
const comm = uni.getStorageSync('changeCommData')
const commName = (comm && comm.name) ? comm.name : ''
if (addr) {
this.serviceAddress = (addr + ' ' + commName).trim()
} else if (commName) {
this.serviceAddress = commName
}
const roomNo = uni.getStorageSync('currentRoomNo')
if (roomNo) this.houseNumber = roomNo
},
// + room_name
loadBoundHouse() {
const loc = uni.getStorageSync('location') || {}
const comm = uni.getStorageSync('changeCommData')
request(commApi.commInfo, 'POST', {
user_id: uni.getStorageSync('userId'),
longitude: loc.lng,
latitude: loc.lat,
page_num: 1,
page_size: 20
}, { silent: false }).then(res => {
const rows = (res && res.rows) ? res.rows : []
if (!rows.length) return
//
let target = null
if (comm && comm.id) target = rows.find(r => r.community_id === comm.id)
if (!target) target = rows[0]
const commName = target.name || ''
if (target.addr) {
this.serviceAddress = (target.addr + ' ' + commName).trim()
} else if (commName) {
this.serviceAddress = commName
}
const owner = (target.room_owner_list && target.room_owner_list.length) ? target.room_owner_list[0] : null
if (owner && owner.room_name) this.houseNumber = owner.room_name
}).catch(() => {})
},
// = + 3
initDefaultTime() {
const d = new Date()
d.setHours(d.getHours() + 3)
// //
if (d.getMinutes() > 0 || d.getSeconds() > 0 || d.getMilliseconds() > 0) {
d.setHours(d.getHours() + 1)
}
d.setMinutes(0, 0, 0)
this.dateValue = d.getTime()
this.serviceTime = this.formatTime(d)
},
formatTime(d) {
const p = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
},
// /
chooseLocation() {
const loc = uni.getStorageSync('location') || {}
uni.chooseLocation({
latitude: loc.lat,
longitude: loc.lng,
success: (res) => {
const detail = res.name ? (res.address + ' ' + res.name) : res.address
this.serviceAddress = (detail || '').trim()
},
fail: (err) => {
if (err && err.errMsg && err.errMsg.indexOf('cancel') === -1) {
uni.showToast({ title: '地图选点失败,请检查定位权限', icon: 'none' })
}
}
})
},
onDate(e) {
const d = new Date(e.value)
this.dateValue = d.getTime()
this.serviceTime = this.formatTime(d)
this.showDate = false
},
submit() {
if (!this.contactName) { uni.showToast({ title: '请输入联系人', icon: 'none' }); return }
if (!isPhone(this.contactPhone)) { uni.showToast({ title: '联系电话格式不正确', icon: 'none' }); return }
if (!this.serviceAddress) { uni.showToast({ title: '请选择或输入小区地址', icon: 'none' }); return }
if (!this.houseNumber) { uni.showToast({ title: '请输入门牌号', icon: 'none' }); return }
if (this.submitting) return
this.submitting = true
const userId = uni.getStorageSync('userId')
const fullAddress = (this.serviceAddress + ' ' + this.houseNumber).trim()
request(apiArr.orderCreate, 'POST', {
user_id: userId,
service_info_id: this.params.service_info_id,
sku_id: this.params.sku_id,
employee_id: this.params.employee_id,
service_address: fullAddress,
contact_name: this.contactName,
contact_phone: this.contactPhone,
service_time: this.serviceTime,
remark: this.remark
}).then(res => {
this.payOrder(res.id, userId)
}).catch(() => { this.submitting = false })
},
payOrder(orderId, userId) {
request(apiArr.preorder, 'POST', { order_id: orderId, user_id: userId, trans_type: '71' }).then(pay => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: pay.timeStamp,
nonceStr: pay.nonceStr,
package: pay.package,
signType: pay.signType || 'RSA',
paySign: pay.paySign,
success: () => {
//
request(apiArr.tradeQuery, 'POST', { order_id: orderId }, {}, false).finally(() => {
uni.redirectTo({ url: '/packages/homeService/myOrders/index' })
})
},
fail: () => {
this.submitting = false
uni.showToast({ title: '支付已取消', icon: 'none' })
uni.redirectTo({ url: '/packages/homeService/myOrders/index' })
}
})
}).catch(() => { this.submitting = false })
}
}
}
</script>
<style scoped>
.hso-page { background: #f5f5f5; min-height: 100vh; padding-bottom: 130rpx; }
.hso-card { background: #fff; padding: 28rpx 24rpx; display: flex; align-items: center; justify-content: space-between; }
.hso-svc { font-size: 30rpx; color: #222; font-weight: 600; }
.hso-deposit { font-size: 30rpx; color: #FF370B; font-weight: 600; }
.hso-form { background: #fff; margin-top: 16rpx; padding: 0 24rpx; }
.hso-row { display: flex; align-items: center; padding: 26rpx 0; border-bottom: 1rpx solid #f2f2f2; }
.hso-row--col { flex-direction: column; align-items: flex-start; }
.hso-label { width: 160rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
.hso-input { flex: 1; font-size: 28rpx; color: #222; }
.hso-input.ph { color: #b7b7b7; }
.hso-map-btn { flex-shrink: 0; margin-left: 16rpx; font-size: 26rpx; color: #FF370B; padding: 6rpx 16rpx; border: 1rpx solid #FF370B; border-radius: 28rpx; }
.hso-textarea { width: 100%; height: 120rpx; font-size: 28rpx; margin-top: 12rpx; }
.hso-bottom { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; display: flex; align-items: center; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.04); }
.hso-bottom-price { flex: 1; font-size: 26rpx; color: #333; }
.hso-bottom-price text { color: #FF370B; font-size: 36rpx; font-weight: 600; }
.hso-bottom-btn { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; font-size: 30rpx; padding: 20rpx 60rpx; border-radius: 44rpx; }
</style>

View File

@ -0,0 +1,66 @@
<template>
<view class="hsod-page" v-if="order.id">
<view class="hsod-status">{{ statusText }}</view>
<view class="hsod-card">
<image class="hsod-pic" :src="picUrl + (order.service_pic || '')" mode="aspectFill" />
<view class="hsod-info">
<view class="hsod-name">{{ order.service_name }}</view>
<view class="hsod-kind">{{ kindText(order.order_kind) }}</view>
<view class="hsod-amount">¥{{ order.amount }}</view>
</view>
</view>
<view class="hsod-block">
<view class="hsod-row"><text>订单号</text><text>{{ order.order_no }}</text></view>
<view class="hsod-row"><text>联系人</text><text>{{ order.contact_name }}</text></view>
<view class="hsod-row"><text>联系电话</text><text>{{ order.contact_phone }}</text></view>
<view class="hsod-row"><text>服务地址</text><text>{{ order.service_address }}</text></view>
<view class="hsod-row" v-if="order.service_time"><text>上门时间</text><text>{{ order.service_time }}</text></view>
<view class="hsod-row" v-if="order.remark"><text>备注</text><text>{{ order.remark }}</text></view>
<view class="hsod-row"><text>支付状态</text><text>{{ order.pay_status === 2 ? '已支付' : '待支付' }}</text></view>
</view>
</view>
</template>
<script>
import { request, picUrl } from '@/utils'
import { apiArr } from '@/api/homeService'
export default {
data() {
return { picUrl, order: {} }
},
onLoad(options) {
request(apiArr.orderInfo, 'POST', { id: Number(options.id) }, {}, false).then(res => {
this.order = res || {}
})
},
computed: {
statusText() {
const o = this.order
if (o.pay_status === 1) return '待支付'
const map = { 1: '待上门', 2: '服务中', 3: '待补款', 4: '已完成', 5: '已取消' }
return map[o.status] || ''
}
},
methods: {
kindText(k) {
return k === 1 ? '定金/上门费' : k === 2 ? '补差/尾款' : k === 3 ? '月账单' : ''
}
}
}
</script>
<style scoped>
.hsod-page { background: #f5f5f5; min-height: 100vh; }
.hsod-status { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; font-size: 34rpx; font-weight: 600; padding: 40rpx 24rpx; }
.hsod-card { background: #fff; display: flex; padding: 24rpx; margin-top: 16rpx; }
.hsod-pic { width: 150rpx; height: 150rpx; border-radius: 8rpx; background: #f0f0f0; flex-shrink: 0; }
.hsod-info { flex: 1; margin-left: 18rpx; display: flex; flex-direction: column; }
.hsod-name { font-size: 30rpx; color: #222; font-weight: 600; }
.hsod-kind { font-size: 24rpx; color: #999; margin-top: 10rpx; flex: 1; }
.hsod-amount { font-size: 32rpx; color: #FF370B; font-weight: 600; }
.hsod-block { background: #fff; margin-top: 16rpx; padding: 8rpx 24rpx; }
.hsod-row { display: flex; justify-content: space-between; font-size: 26rpx; color: #333; padding: 22rpx 0; border-bottom: 1rpx solid #f5f5f5; }
.hsod-row text:first-child { color: #999; }
.hsod-row:last-child { border-bottom: none; }
</style>

View File

@ -0,0 +1,307 @@
<template>
<view class="hsw-page">
<!-- 顶部Tab全部/待上门/服务中/已完成/月账单 -->
<view class="hsw-tabs">
<view
class="hsw-tab"
:class="{ active: statusTab === item.v }"
v-for="item in orderTabs"
:key="item.v"
@tap="switchStatus(item.v)"
>{{ item.t }}</view>
</view>
<!-- 月账单子Tab -->
<view class="hsw-tabs hsw-tabs-bill" v-if="showMonthlyBill">
<view
class="hsw-tab"
:class="{ active: billTab === item.v }"
v-for="item in billTabs"
:key="item.v"
@tap="switchBillTab(item.v)"
>{{ item.t }}</view>
</view>
<!-- 列表区域 -->
<scroll-view class="hsw-list" scroll-y>
<!-- 普通服务单非月账单模式 -->
<block v-if="!showMonthlyBill">
<view class="hsw-card" v-for="o in orders" :key="o.id">
<view class="hsw-head">
<text class="hsw-no">{{ o.order_no }}</text>
<text class="hsw-kind">{{ kindText(o.order_kind) }}</text>
</view>
<view class="hsw-row"><text>服务</text><text>{{ o.service_name }}</text></view>
<view class="hsw-row"><text>客户</text><text>{{ o.contact_name }} {{ o.contact_phone }}</text></view>
<view class="hsw-row"><text>地址</text><text>{{ o.service_address }}</text></view>
<view class="hsw-row"><text>金额</text><text class="hsw-amount">¥{{ o.amount }}</text></view>
<view class="hsw-foot">
<view class="hsw-btn ghost" v-if="o.status === 1" @tap="updateStatus(o, 2)">开始服务</view>
<view class="hsw-btn" v-if="o.status === 2 && o.order_kind === 1" @tap="openExtra(o)">代客补差</view>
<view class="hsw-btn ghost" v-if="o.status === 2" @tap="updateStatus(o, 4)">完成服务</view>
</view>
</view>
<view v-if="orders.length === 0" class="hsw-empty">暂无服务单</view>
</block>
<!-- 月账单列表师傅可见所有状态 -->
<block v-else>
<view class="hsw-card hsw-bill-card" v-for="b in bills" :key="b.id">
<view class="hsw-head">
<text class="hsw-no">{{ b.bill_no }}</text>
<text class="hsw-kind" :class="b.push_status === 0 ? 'status-warn' : b.push_status === 1 ? 'status-info' : b.push_status === 2 ? 'status-ok' : 'status-cancel'">{{ pushStatusText(b.push_status) }}</text>
</view>
<view class="hsw-row"><text>服务</text><text>{{ b.service_name }}</text></view>
<view class="hsw-row"><text>客户</text><text>{{ b.contact_name }} {{ b.contact_phone }}</text></view>
<view class="hsw-row"><text>地址</text><text>{{ b.service_address }}</text></view>
<view class="hsw-row"><text>账单月份</text><text>{{ b.bill_month }}</text></view>
<view class="hsw-row"><text>金额</text><text class="hsw-amount">¥{{ b.amount }}</text></view>
<view class="hsw-foot">
<view class="hsw-btn" v-if="b.push_status === 0" @tap="openPush(b)">推送至用户支付</view>
<view class="hsw-btn ghost" v-if="b.push_status === 0" @tap="openReject(b)">拒绝</view>
<view class="hsw-btn ghost" v-if="b.push_status === 1">已推送待用户支付</view>
<view class="hsw-btn ghost" v-if="b.push_status === 2">用户已支付</view>
<view class="hsw-btn ghost" v-if="b.push_status === 3">已拒绝{{ b.remark ? '' + b.remark : '' }}</view>
</view>
</view>
<view v-if="bills.length === 0" class="hsw-empty">暂无月账单</view>
</block>
</scroll-view>
<!-- 代客补差弹层 -->
<view class="hsw-mask" v-if="showExtra" @tap.self="showExtra = false">
<view class="hsw-dialog">
<view class="hsw-dialog-tit">代客生成补差单</view>
<input class="hsw-dialog-input" type="digit" v-model="extraAmount" placeholder="请输入补差金额" />
<input class="hsw-dialog-input" v-model="extraRemark" placeholder="备注(选填)" />
<view class="hsw-dialog-btns">
<view class="hsw-dialog-btn cancel" @tap="showExtra = false">取消</view>
<view class="hsw-dialog-btn ok" @tap="submitExtra">确定</view>
</view>
</view>
</view>
<!-- 推送月账单弹层 -->
<view class="hsw-mask" v-if="showPush" @tap.self="showPush = false">
<view class="hsw-dialog">
<view class="hsw-dialog-tit">推送月账单</view>
<view class="hsw-dialog-info">确认推送至用户端用户支付后该账单生效</view>
<view class="hsw-dialog-btns">
<view class="hsw-dialog-btn cancel" @tap="showPush = false">取消</view>
<view class="hsw-dialog-btn ok" @tap="submitPush">确认推送</view>
</view>
</view>
</view>
<!-- 拒绝月账单弹层 -->
<view class="hsw-mask" v-if="showReject" @tap.self="showReject = false">
<view class="hsw-dialog">
<view class="hsw-dialog-tit">拒绝月账单</view>
<input class="hsw-dialog-input" v-model="rejectRemark" placeholder="请输入拒绝原因(选填)" />
<view class="hsw-dialog-btns">
<view class="hsw-dialog-btn cancel" @tap="showReject = false">取消</view>
<view class="hsw-dialog-btn ok" @tap="submitReject">确认拒绝</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { request } from '@/utils'
import { apiArr } from '@/api/homeService'
export default {
data() {
return {
employeeId: 0,
statusTab: 0,
orderTabs: [
{ v: 0, t: '全部' },
{ v: 1, t: '待上门' },
{ v: 2, t: '服务中' },
{ v: 4, t: '已完成' },
{ v: -1, t: '月账单' }
],
showMonthlyBill: false,
billTab: 0,
billTabs: [
{ v: 0, t: '全部' },
{ v: 1, t: '待推送' },
{ v: 2, t: '已推送' }
],
orders: [],
bills: [],
showExtra: false,
extraOrder: null,
extraAmount: '',
extraRemark: '',
showPush: false,
showReject: false,
pushBill: null,
rejectBill: null,
rejectRemark: ''
}
},
onLoad(options) {
this.employeeId = Number(options.employeeId || uni.getStorageSync('serviceEmployeeId') || 0)
if (!this.employeeId) {
const userId = uni.getStorageSync('userId')
request(apiArr.workerMyInfo, 'POST', { user_id: userId }, {}, false).then(res => {
if (res && res.is_worker && res.employee_id) {
this.employeeId = res.employee_id
uni.setStorageSync('serviceEmployeeId', res.employee_id)
this.loadOrders()
} else {
uni.showModal({ title: '提示', content: '您还不是服务师傅,请联系商家添加', showCancel: false })
}
})
}
},
onShow() {
if (this.employeeId) {
if (this.showMonthlyBill) {
this.loadBills()
} else {
this.loadOrders()
}
}
},
methods: {
switchStatus(v) {
if (v === -1) {
this.showMonthlyBill = true
this.billTab = 0
this.loadBills()
} else {
this.showMonthlyBill = false
this.statusTab = v
this.loadOrders()
}
},
switchBillTab(v) {
this.billTab = v
this.loadBills()
},
loadOrders() {
request(apiArr.workerOrderList, 'POST', {
employee_id: this.employeeId,
status: this.statusTab,
page_num: 1,
page_size: 50
}, {}, false).then(res => {
this.orders = res.rows || []
})
},
loadBills() {
request(apiArr.workerMonthlyBillList, 'POST', {
employee_id: this.employeeId,
user_id: Number(uni.getStorageSync('userId')) || 0,
page_num: 1,
page_size: 50
}, {}, false).then(res => {
let rows = res.rows || []
if (this.billTab === 1) rows = rows.filter(b => b.push_status === 0)
if (this.billTab === 2) rows = rows.filter(b => b.push_status === 1 || b.push_status === 2)
this.bills = rows
})
},
kindText(k) {
return k === 1 ? '定金/上门费' : k === 2 ? '补差/尾款' : k === 3 ? '月账单' : ''
},
pushStatusText(s) {
return s === 0 ? '待推送' : s === 1 ? '已推送待支付' : s === 2 ? '已支付' : s === 3 ? '已拒绝' : ''
},
updateStatus(o, status) {
request(apiArr.workerOrderStatus, 'POST', { order_id: o.id, status }).then(() => {
uni.showToast({ title: '操作成功', icon: 'none' })
this.loadOrders()
})
},
openExtra(o) {
this.extraOrder = o
this.extraAmount = ''
this.extraRemark = ''
this.showExtra = true
},
submitExtra() {
const amt = Number(this.extraAmount)
if (!amt || amt <= 0) { uni.showToast({ title: '请输入补差金额', icon: 'none' }); return }
request(apiArr.workerExtraOrder, 'POST', {
order_id: this.extraOrder.id,
amount: amt,
remark: this.extraRemark
}).then(() => {
this.showExtra = false
uni.showToast({ title: '已生成补差单,待客户支付', icon: 'none' })
this.loadOrders()
})
},
openPush(b) {
this.pushBill = b
this.showPush = true
},
submitPush() {
request(apiArr.workerPushBill, 'POST', {
bill_id: this.pushBill.id,
action: 1
}).then(() => {
this.showPush = false
uni.showToast({ title: '已推送,用户可支付', icon: 'none' })
this.loadBills()
}).catch(err => {
uni.showToast({ title: err.errMsg || '操作失败', icon: 'none' })
})
},
openReject(b) {
this.rejectBill = b
this.rejectRemark = ''
this.showReject = true
},
submitReject() {
request(apiArr.workerPushBill, 'POST', {
bill_id: this.rejectBill.id,
action: 3,
remark: this.rejectRemark
}).then(() => {
this.showReject = false
uni.showToast({ title: '已拒绝', icon: 'none' })
this.loadBills()
}).catch(err => {
uni.showToast({ title: err.errMsg || '操作失败', icon: 'none' })
})
}
}
}
</script>
<style scoped>
.hsw-page { display: flex; flex-direction: column; height: 100vh; background: #f5f5f5; }
.hsw-tabs { display: flex; background: #fff; }
.hsw-tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 26rpx; color: #666; }
.hsw-tab.active { color: #FF370B; font-weight: 600; border-bottom: 4rpx solid #FF370B; }
.hsw-tabs-bill { border-top: 1rpx solid #f0f0f0; }
.hsw-list { flex: 1; padding: 20rpx; box-sizing: border-box; }
.hsw-card { background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 18rpx; }
.hsw-bill-card { border-left: 6rpx solid #FF370B; }
.hsw-head { display: flex; justify-content: space-between; font-size: 24rpx; color: #999; margin-bottom: 14rpx; }
.hsw-kind { color: #FF370B; }
.status-warn { color: #FF9800; }
.status-info { color: #2196F3; }
.status-ok { color: #4CAF50; }
.status-cancel { color: #999; }
.hsw-row { display: flex; justify-content: space-between; font-size: 26rpx; color: #333; padding: 8rpx 0; }
.hsw-row text:first-child { color: #999; }
.hsw-amount { color: #FF370B; font-weight: 600; }
.hsw-foot { display: flex; justify-content: flex-end; gap: 16rpx; margin-top: 16rpx; }
.hsw-btn { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; font-size: 26rpx; padding: 12rpx 36rpx; border-radius: 40rpx; }
.hsw-btn.ghost { background: #fff; color: #FF370B; border: 1rpx solid #FF370B; }
.hsw-empty { text-align: center; color: #999; font-size: 26rpx; padding: 80rpx 0; }
.hsw-mask { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 999; }
.hsw-dialog { width: 600rpx; background: #fff; border-radius: 16rpx; padding: 32rpx; }
.hsw-dialog-tit { font-size: 30rpx; font-weight: 600; text-align: center; margin-bottom: 24rpx; }
.hsw-dialog-info { font-size: 26rpx; color: #666; text-align: center; margin-bottom: 24rpx; }
.hsw-dialog-input { border: 1rpx solid #eee; border-radius: 8rpx; height: 80rpx; padding: 0 20rpx; font-size: 28rpx; margin-bottom: 18rpx; }
.hsw-dialog-btns { display: flex; gap: 20rpx; margin-top: 10rpx; }
.hsw-dialog-btn { flex: 1; text-align: center; padding: 18rpx 0; border-radius: 44rpx; font-size: 28rpx; }
.hsw-dialog-btn.cancel { background: #f2f2f2; color: #666; }
.hsw-dialog-btn.ok { background: linear-gradient(91deg, #FF7658, #FF370B); color: #fff; }
</style>

View File

@ -426,3 +426,83 @@ page {
.coupon_item_button::after {
border: none;
}
/* ============ 店铺活动商品 ============ */
.shop-activity {
padding: 24rpx;
}
.shop-activity-title {
font-size: 32rpx;
font-weight: 600;
color: #222;
padding-left: 16rpx;
border-left: 6rpx solid #FF370B;
margin-bottom: 20rpx;
}
.shop-activity-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.sa-goods {
display: flex;
align-items: center;
background: #fff;
border: 1rpx solid #f2f2f2;
border-radius: 12rpx;
padding: 16rpx;
}
.sa-pic {
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
flex-shrink: 0;
background: #f5f5f5;
}
.sa-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
}
.sa-name {
font-size: 28rpx;
color: #222;
line-height: 38rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.sa-price-row {
display: flex;
align-items: baseline;
margin-top: 16rpx;
gap: 16rpx;
}
.sa-price {
font-size: 34rpx;
font-weight: 600;
color: #FF370B;
}
.sa-unit {
font-size: 22rpx;
font-weight: normal;
}
.sa-origin {
font-size: 22rpx;
color: #999;
text-decoration: line-through;
}

View File

@ -59,6 +59,27 @@
</view>
</view>
<!-- 店铺活动商品 -->
<view class="white_container shop-activity" v-if="shopActivityList.length > 0">
<view class="shop-activity-title">店铺活动</view>
<view class="shop-activity-list">
<block v-for="(item, index) in shopActivityList" :key="index">
<view class="sa-goods" v-for="(sku, sIdx) in item.group_buy_goods_list" :key="sIdx"
@click="toShopActivityDetail(sku)">
<image class="sa-pic" :src="sku.commodity_pic || item.commodity_pic" mode="aspectFill" />
<view class="sa-info">
<view class="sa-name">{{ sku.goods_name || item.commodity_name }}</view>
<view class="sa-price-row">
<view class="sa-price">{{ sku.group_buy_price }}<text class="sa-unit" v-if="sku.goods_unit">/{{ sku.goods_unit }}</text></view>
<view class="sa-origin" v-if="sku.sales_price">单买价 {{ sku.sales_price }}</view>
</view>
</view>
</view>
</block>
</view>
</view>
<view class="white_container detail" v-for="(item, index) in commentList" :key="index">
<view class="Msg">
<view class="Msg_Tit">
@ -116,7 +137,7 @@
<image src="https://static.hshuishang.com/property-img-file/localLife_detail_Frame.png" mode="" />
<view>电话</view>
</view>
<view class="left_label" @click="handleDiscussClick">
<view v-if="isShow" class="left_label" @click="handleDiscussClick">
<image src="https://static.hshuishang.com/property-img-file/localLife_shopList_Group_1334.png" mode="" />
<view>点评</view>
</view>
@ -181,13 +202,30 @@ export default {
isDisabled: false,
coupons: [],
couponDetails: [],
showCouponPopup: false
showCouponPopup: false,
shopActivityList: [] // activity_type=2
};
},
onLoad(options) {
const params = {
id: Number(uni.getStorageSync("merchantInfo").id)
}
let id;
if (options.scene) {
const scene = decodeURIComponent(options.scene);
function getQueryParam(str, name) {
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`);
const match = str.match(reg);
return match ? decodeURIComponent(match[2]) : null;
}
const idStr = getQueryParam(scene, 'id'); // "22"
if (idStr) {
id = Number(idStr); // 22
}
}else{
id = Number(uni.getStorageSync("merchantInfo").id)
}
console.log('scene:',options.scene,id)
const params = {
id: id
}
request(apiArr.getMerchantInfo, "POST", params).then(res => {
this.info = res
uni.setStorageSync('merchantInfo', res)
@ -197,7 +235,8 @@ export default {
this.flag = false
this.commentList = []
this.getCommentList()
})
this.getShopActivity()
});
},
onShow() {
if (this.info.id) {
@ -213,6 +252,46 @@ export default {
},
methods: {
// activity_type=2
getShopActivity() {
const userId = uni.getStorageSync('userId')
if (!userId) return
const params = {
user_id: userId,
merchant_id: this.info.id,
community_id: uni.getStorageSync('changeCommData') ? uni.getStorageSync('changeCommData').id : ''
}
request(apiArr.getShopActivityList, "POST", params, {}, false).then(res => {
const list = (res.list || []).map(item => {
const group_buy_goods_list = (item.group_buy_goods_list || []).map(sku => ({
...sku,
commodity_pic: picUrl + sku.commodity_pic,
quantity: 0
}))
return {
...item,
commodity_pic: picUrl + item.commodity_pic,
group_buy_goods_list
}
})
this.shopActivityList = list
}).catch(() => {
this.shopActivityList = []
})
},
// ->
toShopActivityDetail(itemObj) {
let targetItem = itemObj
if (!targetItem.group_buy_activity_info && targetItem.group_buy_goods_list && targetItem.group_buy_goods_list.length > 0) {
targetItem = targetItem.group_buy_goods_list[0]
}
if (!targetItem.group_buy_activity_info) return
const item = {
...targetItem,
groupById: targetItem.group_buy_activity_info.id
}
NavgateTo(`/packages/shop/groupPurchaseDetail/index?item=${JSON.stringify(item)}`)
},
getCouponList() {
const params = {
mch_id: uni.getStorageSync("merchantInfo").id,

View File

@ -83,7 +83,7 @@
买单返积分
</view>
</view>
<view class="merchantItem_right_con_right" @click="toJump(item)">
<view v-if="item.quick_purchase_enabled==1" class="merchantItem_right_con_right" @click.stop="toJump(item)">
<image src="https://static.hshuishang.com/property-img-file/local_review.png"
mode="aspectFill"></image>
点评
@ -119,6 +119,7 @@
import {
request,
picUrl,
RequestUrl,
uniqueByField,
menuButtonInfo,
calculateDistance,
@ -155,8 +156,20 @@ export default {
console.log(this.address);
},
onPullDownRefresh() {
this.page_num = 1;
this.merchatList = [];
this.flag = false;
this.searchText = '';
this.getCateList();
this.getMechantList().then(() => {
uni.stopPullDownRefresh();
});
},
onReachBottom() {
if (this.flag) {
this.page_num += 1;
this.getMechantList();
}
},

View File

@ -170,7 +170,15 @@ export default {
};
},
onLoad(options) {
const item = JSON.parse(options?.item);
let item
// URL
if (options && options.fromChat) {
try { item = uni.getStorageSync('chatCardItem') } catch (e) { item = null }
uni.removeStorageSync('chatCardItem')
}
if (!item) {
item = JSON.parse(options?.item);
}
this.orderInfo = item;
//
item.order_status == "1" ? this.startCountdown() : "";

View File

@ -0,0 +1,16 @@
<template>
<view class="container">
<view>提交订单</view>
</view>
</template>
<script>
export default {
data() {
return {};
},
};
</script>
<style scoped>
</style>

View File

@ -1,67 +1,148 @@
/* ============================================================
* 商家入驻 - 资料审核状态页
* 审核中 #FF9F0A 通过 #34C759 失败 #FF3B30 主色 #FF370B
* ============================================================ */
page {
background-color: #F6F7F9;
min-height: 100vh;
}
.container {
margin-top: 100rpx;
display: flex;
justify-content: center;
padding: 24rpx;
box-sizing: border-box;
padding-bottom: 200rpx;
}
.auditStatus {
width: 100px;
height: 100px;
margin: 0 auto;
.audit-card {
background: #FFFFFF;
border-radius: 16rpx;
padding: 60rpx 40rpx 48rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.title {
font-size: 40rpx;
font-weight: bold;
margin: 10rpx 0;
text-align: center;
.audit-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 28rpx;
}
.content{
font-size: 26rpx;
margin-top: 40rpx;
text-align: center;
color: #a9a9a9;
.audit-icon--ing {
background: #FFF4E0;
}
.info{
margin-top: 70rpx;
width: 500rpx;
background-color: #f6f6fa;
padding: 20rpx 30rpx;
border-radius: 20rpx;
.audit-icon--ok {
background: #E6F8EA;
}
.info view{
display: flex;
justify-content: space-between;
margin: 15rpx 0;
.audit-icon--fail {
background: #FDECEA;
}
.info_text{
color: #999999;
.audit-title {
font-size: 36rpx;
font-weight: 600;
color: #222;
margin-bottom: 16rpx;
}
.info_text2{
color: #faba5a;
.audit-desc {
font-size: 26rpx;
color: #999;
text-align: center;
line-height: 40rpx;
padding: 0 20rpx;
margin-bottom: 32rpx;
}
.btn{
margin-top: 50rpx;
border: none;
background-color: #ff4218;
color: #ffffff;
border-radius: 50rpx;
.audit-times {
width: 100%;
border-top: 1rpx solid #F2F2F2;
padding-top: 24rpx;
}
.warning{
color: #ff4218;
font-size: 27rpx;
margin: 50rpx 0;
.audit-time-row {
display: flex;
justify-content: center;
font-size: 26rpx;
color: #666;
line-height: 44rpx;
}
.warning_title{
font-size: 30rpx;
font-weight: bold;
margin-bottom: 20rpx;
/* 驳回原因框 */
.reject-box {
width: 100%;
background: #FFF7F4;
border: 1rpx solid #FFD9CD;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.reject-title {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 28rpx;
font-weight: 600;
color: #FF370B;
margin-bottom: 14rpx;
}
.reject-text {
font-size: 26rpx;
color: #FF370B;
line-height: 42rpx;
white-space: pre-wrap;
}
.reject-imgs {
margin-top: 18rpx;
}
.reject-imgs-label {
font-size: 24rpx;
color: #999;
margin-bottom: 12rpx;
}
.reject-thumbs {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.reject-thumb {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
border: 1rpx solid #EEE;
}
/* 底部按钮 */
.audit-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #FFFFFF;
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
z-index: 99;
}
.btn-primary {
height: 88rpx;
background: linear-gradient(91deg, #FF7658 0%, #FF370B 100%);
border-radius: 44rpx;
color: #FFFFFF;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -1,117 +1,141 @@
<template>
<view class="container">
<view class="auditStatusContainer" v-if="itemObj.status == 1">
<view class="header">
<view class="statusIcon">
<image src="https://static.hshuishang.com/enter_audit1.png" mode="aspectFill" class="auditStatus" />
</view>
<view class="title">
审核中
</view>
</view>
<view class="content">
<view>您的入驻申请正在审核中</view>
<view>请耐心等待工作人员处理</view>
</view>
<view class="info">
<view>
<view class="info_text">申请提交时间</view>
<view>{{ itemObj.create_time }}</view>
</view>
<view>
<view class="info_text">当前审核环节</view>
<view class="info_text2">资质验证中</view>
</view>
</view>
</view>
<view class="auditStatusContainer" v-if="itemObj.status == 2">
<view class="header">
<view class="statusIcon">
<image src="https://static.hshuishang.com/af_√.png" mode="aspectFill"
class="auditStatus" />
</view>
<view class="title">
入驻成功
</view>
</view>
<view class="content">
<view>恭喜您您的店铺已成功入驻我们平台</view>
</view>
<view class="info">
<view>
<view class="info_text">店铺名称</view>
<view>{{ itemObj.merchant_name }}</view>
</view>
<view>
<view class="info_text">入住时间</view>
<view>{{ itemObj.handle_time || '' }}</view>
</view>
<view>
<view class="info_text">店铺ID</view>
<view>{{ itemObj.merchant_code }}</view>
</view>
</view>
<!-- <view>
<button class="btn" @click="goShopManage">进入店铺管理</button>
</view> -->
</view>
<view class="auditStatusContainer" v-if="itemObj.status == 3">
<view class="header">
<view class="statusIcon">
<image src="https://static.hshuishang.com/enter_audit2.png" mode="aspectFill" class="auditStatus" />
</view>
<view class="title">
审核失败
</view>
</view>
<view class="content">
<view>很抱歉您的入驻申请未通过审核</view>
<view>请修改后重新提交</view>
</view>
<view class="info">
<view>
<view class="info_text">申请提交时间</view>
<view>{{ itemObj.create_time }}</view>
</view>
<view>
<view class="info_text">审核完成时间</view>
<view>{{ itemObj.handle_time || '' }}</view>
</view>
</view>
<view class="warning" v-if="itemObj.remark">
<view class="warning_title">审核未通过原因</view>
<view>
{{ itemObj.remark || '' }}
</view>
</view>
<view>
<button class="btn" @click="resubmit">修改并重新提交</button>
</view>
</view>
</view>
<view class="container">
<!-- 审核中 -->
<view class="audit-card" v-if="status === 1">
<view class="audit-icon audit-icon--ing">
<u-icon name="clock" color="#FF9F0A" size="56"></u-icon>
</view>
<view class="audit-title">正在审核中</view>
<view class="audit-desc">您的店铺资料已提交工作人员将在 1-3 个工作日内完成审核请耐心等待</view>
<view class="audit-times">
<view class="audit-time-row"><text>申请时间</text><text>{{ itemObj.create_time || '—' }}</text></view>
</view>
</view>
<!-- 审核通过 -->
<view class="audit-card" v-else-if="status === 2">
<view class="audit-icon audit-icon--ok">
<u-icon name="checkmark" color="#34C759" size="56"></u-icon>
</view>
<view class="audit-title">审核通过</view>
<view class="audit-desc">恭喜您您的店铺资料已同意审核</view>
<view class="audit-times">
<view class="audit-time-row"><text>申请时间</text><text>{{ itemObj.create_time || '—' }}</text></view>
<view class="audit-time-row"><text>审核时间</text><text>{{ itemObj.handle_time || '—' }}</text></view>
</view>
</view>
<!-- 审核失败 -->
<view class="audit-card" v-else-if="status === 3">
<view class="audit-icon audit-icon--fail">
<u-icon name="close" color="#FF3B30" size="56"></u-icon>
</view>
<view class="audit-title">审核失败</view>
<view class="audit-desc">很抱歉您的资料未通过审核请修改后重新提交</view>
<view class="reject-box" v-if="itemObj.remark || rejectImages.length">
<view class="reject-title">
<u-icon name="error-circle" color="#FF370B" size="28"></u-icon>
<text>驳回原因</text>
</view>
<view class="reject-text">{{ itemObj.remark || '—' }}</view>
<view class="reject-imgs" v-if="rejectImages.length">
<view class="reject-imgs-label">问题截图</view>
<view class="reject-thumbs">
<image v-for="(u, i) in rejectImages" :key="i" :src="u" class="reject-thumb"
mode="aspectFill" @click="preview(rejectImages, i)" />
</view>
</view>
</view>
<view class="audit-times">
<view class="audit-time-row"><text>申请时间</text><text>{{ itemObj.create_time || '—' }}</text></view>
<view class="audit-time-row"><text>审核时间</text><text>{{ itemObj.handle_time || '—' }}</text></view>
</view>
</view>
<view class="audit-footer" v-if="status === 3">
<view class="btn-primary" @click="resubmit">修改资料并重新提交</view>
</view>
</view>
</template>
<script>
import { request, NavgateTo } from '../../../utils/index';
import { apiArr } from '../../../api/v2Home.js'
import { NavgateTo } from '../../../utils/index';
import { signPrivateView } from '../../../utils/uploadOSS.js';
export default {
data() {
return {
itemObj: {}
}
},
onLoad(options) {
this.itemObj = JSON.parse(options.itemObj);
},
methods: {
goShopManage() {
NavgateTo('/packages/storeManagement/index/index');
},
resubmit() {
NavgateTo('/packages/shopEnter/index/index?itemObj=' + JSON.stringify(this.itemObj));
}
}
data() {
return {
itemObj: {},
rejectImages: [],
}
},
computed: {
status() {
return Number(this.itemObj.status) || 0
}
},
onLoad(options) {
if (options && options.itemObj) {
try {
this.itemObj = JSON.parse(decodeURIComponent(options.itemObj))
} catch (e) {
console.error('解析审核状态参数失败:', e)
this.itemObj = {}
}
} else if (options && options.status) {
// status
this.itemObj = { status: Number(options.status) }
}
// reject_images key
if (this.itemObj.reject_images) {
this.signRejectImages(this.itemObj.reject_images)
}
},
methods: {
async signRejectImages(paths) {
const list = (paths || '').split(',').filter(Boolean)
const result = []
for (const p of list) {
try {
const r = await signPrivateView(p)
result.push(r.url)
} catch (e) {
console.error('签发驳回截图 URL 失败:', p, e)
}
}
this.rejectImages = result
},
preview(urls, current) {
if (!urls || !urls.length) return
uni.previewImage({ urls, current: urls[current] })
},
resubmit() {
// URL
try {
uni.setStorageSync('shopEnterEditData', this.itemObj)
} catch (e) {
console.error('暂存编辑数据失败:', e)
}
NavgateTo('/packages/shopEnter/index/index?edit=1');
},
// 退/
goHome() {
uni.reLaunch({ url: '/pages/index/index' })
}
},
// /
onBackPress(e) {
// status 3
if (e && e.from === 'backbutton') {
uni.reLaunch({ url: '/pages/index/index' })
return true
}
return false
}
}
</script>
<style>
@import url("./index.css");
</style>

View File

@ -0,0 +1,235 @@
/* ============================================================
* 商家入驻 - 自定义相机取景框拍证件
* ============================================================ */
page {
background: #000;
}
.cam-page {
position: fixed;
inset: 0;
background: #000;
overflow: hidden;
}
/* 顶部导航 */
.cam-nav {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx 24rpx;
height: 88rpx;
}
.cam-nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
}
.cam-nav-title {
color: #fff;
font-size: 32rpx;
font-weight: 500;
}
.cam-nav-right {
width: 60rpx;
}
/* 相机取景 */
.cam-view {
width: 100%;
height: 100vh;
}
/* 取景框遮罩 */
.cam-overlay {
position: absolute;
inset: 0;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
}
.cam-frame {
position: relative;
border: 2rpx dashed rgba(255, 255, 255, 0.7);
border-radius: 16rpx;
}
.frame-landscape {
width: 86vw;
height: 54vw;
}
.frame-portrait {
width: 64vw;
height: 86vw;
}
.cam-corner {
position: absolute;
width: 44rpx;
height: 44rpx;
border-color: #FF370B;
border-style: solid;
border-width: 0;
}
.cam-corner.tl {
top: -2rpx;
left: -2rpx;
border-top-width: 6rpx;
border-left-width: 6rpx;
border-top-left-radius: 16rpx;
}
.cam-corner.tr {
top: -2rpx;
right: -2rpx;
border-top-width: 6rpx;
border-right-width: 6rpx;
border-top-right-radius: 16rpx;
}
.cam-corner.bl {
bottom: -2rpx;
left: -2rpx;
border-bottom-width: 6rpx;
border-left-width: 6rpx;
border-bottom-left-radius: 16rpx;
}
.cam-corner.br {
bottom: -2rpx;
right: -2rpx;
border-bottom-width: 6rpx;
border-right-width: 6rpx;
border-bottom-right-radius: 16rpx;
}
.cam-tip {
margin-top: 36rpx;
color: #fff;
font-size: 28rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.6);
}
/* 横版/竖版切换 */
.cam-orient {
position: absolute;
bottom: 280rpx;
left: 0;
right: 0;
z-index: 20;
display: flex;
justify-content: center;
gap: 24rpx;
}
.orient-item {
padding: 12rpx 40rpx;
border-radius: 40rpx;
background: rgba(255, 255, 255, 0.18);
color: rgba(255, 255, 255, 0.8);
font-size: 28rpx;
}
.orient-item.active {
background: #FF370B;
color: #fff;
}
/* 底部操作 */
.cam-actions {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 20;
height: 240rpx;
display: flex;
align-items: center;
justify-content: space-around;
padding-bottom: env(safe-area-inset-bottom);
background: rgba(0, 0, 0, 0.3);
}
.cam-action-side {
width: 110rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
color: #fff;
font-size: 24rpx;
}
.cam-action-placeholder {
opacity: 0;
}
.cam-shutter {
width: 130rpx;
height: 130rpx;
border-radius: 50%;
background: #fff;
border: 8rpx solid rgba(255, 255, 255, 0.4);
box-sizing: border-box;
}
.cam-shutter:active {
transform: scale(0.94);
}
/* 预览态 */
.cam-preview {
width: 100%;
height: 100vh;
background: #000;
}
.cam-preview-actions {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 20;
height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 40rpx;
padding-bottom: env(safe-area-inset-bottom);
background: rgba(0, 0, 0, 0.4);
}
.prev-btn {
width: 240rpx;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
}
.prev-retake {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.prev-submit {
background: linear-gradient(91deg, #FF7658 0%, #FF370B 100%);
color: #fff;
}

View File

@ -0,0 +1,165 @@
<template>
<view class="cam-page">
<!-- 顶部自定义导航 -->
<view class="cam-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="cam-nav-back" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="40"></u-icon>
</view>
<view class="cam-nav-title">{{ title }}</view>
<view class="cam-nav-right"></view>
</view>
<!-- 拍摄态相机取景 -->
<block v-if="!captured">
<camera class="cam-view" :device-position="'back'" :flash="'auto'" @error="onCamError"></camera>
<!-- 取景框遮罩 -->
<view class="cam-overlay">
<view class="cam-frame" :class="orientation === 'landscape' ? 'frame-landscape' : 'frame-portrait'">
<view class="cam-corner tl"></view>
<view class="cam-corner tr"></view>
<view class="cam-corner bl"></view>
<view class="cam-corner br"></view>
</view>
<view class="cam-tip">{{ frameTip }}</view>
</view>
<!-- 横版/竖版切换 -->
<view class="cam-orient">
<view class="orient-item" :class="{ active: orientation === 'landscape' }" @click="orientation = 'landscape'">横版</view>
<view class="orient-item" :class="{ active: orientation === 'portrait' }" @click="orientation = 'portrait'">竖版</view>
</view>
<!-- 底部操作相册 / 拍照 -->
<view class="cam-actions">
<view class="cam-action-side" @click="pickFromAlbum">
<u-icon name="photo" color="#fff" size="44"></u-icon>
<text>相册</text>
</view>
<view class="cam-shutter" @click="takePhoto"></view>
<view class="cam-action-side cam-action-placeholder"></view>
</view>
</block>
<!-- 预览态重拍 / 提交 -->
<block v-else>
<image class="cam-preview" :src="tempPath" mode="aspectFit"></image>
<view class="cam-preview-actions">
<view class="prev-btn prev-retake" @click="retake">重拍</view>
<view class="prev-btn prev-submit" @click="confirmUse">提交</view>
</view>
</block>
</view>
</template>
<script>
export default {
data() {
return {
statusBarHeight: 20,
target: 'license', // license / idFront / idBack / industry
orientation: 'landscape',
captured: false,
tempPath: '',
cameraCtx: null,
}
},
computed: {
title() {
const map = {
license: '拍摄营业执照',
idFront: '拍摄身份证人像面',
idBack: '拍摄身份证国徽面',
industry: '拍摄行业资质',
}
return map[this.target] || '拍摄证件'
},
frameTip() {
const map = {
license: '请将营业执照完整置于取景框内',
idFront: '请将身份证人像面置于取景框内',
idBack: '请将身份证国徽面置于取景框内',
industry: '请将资质证件完整置于取景框内',
}
return map[this.target] || '请将证件完整置于取景框内'
}
},
onLoad(options) {
if (options && options.target) this.target = options.target
// /
this.orientation = (this.target === 'idFront' || this.target === 'idBack') ? 'landscape' : 'portrait'
try {
const sys = uni.getSystemInfoSync()
this.statusBarHeight = sys.statusBarHeight || 20
} catch (e) {}
},
onReady() {
// #ifndef H5
this.cameraCtx = uni.createCameraContext()
// #endif
},
methods: {
goBack() {
uni.navigateBack()
},
onCamError(e) {
console.error('相机错误:', e)
uni.showModal({
title: '无法使用相机',
content: '请检查是否已授予相机权限,或改用相册上传。',
showCancel: false
})
},
takePhoto() {
if (!this.cameraCtx) {
// H5 camera 退
this.pickFromCamera()
return
}
this.cameraCtx.takePhoto({
quality: 'high',
success: (res) => {
this.tempPath = res.tempImagePath
this.captured = true
},
fail: () => {
uni.showToast({ title: '拍照失败,请重试', icon: 'none' })
}
})
},
pickFromCamera() {
uni.chooseImage({
count: 1, sizeType: ['compressed'], sourceType: ['camera'],
success: (res) => {
const fp = res.tempFilePaths && res.tempFilePaths[0]
if (!fp) return
this.tempPath = fp
this.captured = true
}
})
},
pickFromAlbum() {
uni.chooseImage({
count: 1, sizeType: ['compressed'], sourceType: ['album'],
success: (res) => {
const fp = res.tempFilePaths && res.tempFilePaths[0]
if (!fp) return
this.tempPath = fp
this.captured = true
}
})
},
retake() {
this.captured = false
this.tempPath = ''
},
confirmUse() {
if (!this.tempPath) return
// 线 + OCR
uni.$emit('shopEnter:capture', { target: this.target, filePath: this.tempPath })
uni.navigateBack()
}
}
}
</script>
<style>
@import url("./index.css");
</style>

View File

@ -0,0 +1,95 @@
page {
background-color: #f6f7fb;
min-height: 100vh;
}
.container {
padding: 30rpx;
}
.header {
padding: 40rpx 0 60rpx;
text-align: center;
}
.header-title {
font-size: 40rpx;
font-weight: bold;
color: #222222;
margin-bottom: 16rpx;
}
.header-desc {
font-size: 28rpx;
color: #999999;
}
.type-list {
margin-bottom: 40rpx;
}
.type-card {
display: flex;
align-items: center;
background: #FFFFFF;
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.type-card:active {
opacity: 0.8;
}
.type-icon {
width: 100rpx;
height: 100rpx;
margin-right: 24rpx;
flex-shrink: 0;
}
.type-icon image {
width: 100%;
height: 100%;
}
.type-info {
flex: 1;
}
.type-name {
font-size: 32rpx;
font-weight: bold;
color: #222222;
margin-bottom: 10rpx;
}
.type-desc {
font-size: 24rpx;
color: #999999;
}
.type-arrow {
flex-shrink: 0;
}
.tips {
background: #FFFFFF;
border-radius: 20rpx;
padding: 30rpx;
}
.tips-title {
font-size: 28rpx;
font-weight: bold;
color: #222222;
margin-bottom: 20rpx;
}
.tips-item {
font-size: 24rpx;
color: #666666;
line-height: 1.8;
margin-bottom: 10rpx;
}

View File

@ -0,0 +1,63 @@
<template>
<view class="container">
<view class="header">
<view class="header-title">选择入驻类型</view>
<view class="header-desc">请选择您的商家入驻身份</view>
</view>
<view class="type-list">
<!-- 个人入驻 -->
<view class="type-card" @click="chooseType('personal')">
<view class="type-icon">
<image src="https://static.hshuishang.com/property-img-file/icon_personal.png" mode="aspectFit"></image>
</view>
<view class="type-info">
<view class="type-name">个人入驻</view>
<view class="type-desc">适用于个体工商户自然人经营者</view>
</view>
<view class="type-arrow">
<u-icon name="arrow-right" color="#999" size="28"></u-icon>
</view>
</view>
<!-- 企业入驻 -->
<view class="type-card" @click="chooseType('enterprise')">
<view class="type-icon">
<image src="https://static.hshuishang.com/property-img-file/icon_enterprise.png" mode="aspectFit"></image>
</view>
<view class="type-info">
<view class="type-name">企业入驻</view>
<view class="type-desc">适用于企业公司法人</view>
</view>
<view class="type-arrow">
<u-icon name="arrow-right" color="#999" size="28"></u-icon>
</view>
</view>
</view>
<view class="tips">
<view class="tips-title">入驻须知</view>
<view class="tips-item">1. 个人入驻需提供身份证银行卡等个人资质信息</view>
<view class="tips-item">2. 企业入驻需提供营业执照法人身份证对公账户等企业资质信息</view>
<view class="tips-item">3. 提交后将由平台工作人员进行审核预计1-3个工作日</view>
</view>
</view>
</template>
<script>
import { NavgateTo } from '../../../utils';
export default {
data() {
return {}
},
methods: {
chooseType(type) {
NavgateTo('/packages/shopEnter/index/index?enterType=' + type);
}
}
}
</script>
<style>
@import url("./index.css");
</style>

View File

@ -0,0 +1,182 @@
/* ============================================================
* 商家入驻 - 确认信息页预览信息
* 主色 #FF370B 背景 #F6F7F9
* ============================================================ */
page {
background-color: #F6F7F9;
min-height: 100vh;
padding-bottom: 200rpx;
}
.container {
padding-bottom: 40rpx;
box-sizing: border-box;
}
/* 顶部进度条(复用主页样式) */
.step-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx 28rpx;
background: linear-gradient(91deg, #FF7658 0%, #FF370B 100%);
}
.step-pill {
flex: 1;
height: 60rpx;
margin: 0 8rpx;
border-radius: 30rpx;
background: rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.65);
font-size: 26rpx;
}
.step-pill.active {
background: #FFFFFF;
color: #FF370B;
font-weight: 600;
}
.confirm-head {
font-size: 32rpx;
font-weight: 600;
color: #222;
padding: 28rpx 24rpx 8rpx;
}
/* 分组卡片 */
.cf-group {
background: #FFFFFF;
margin: 16rpx 24rpx 0;
border-radius: 16rpx;
padding: 8rpx 24rpx;
}
.cf-group-title {
font-size: 28rpx;
font-weight: 600;
color: #222;
padding: 20rpx 0 12rpx;
border-bottom: 1rpx solid #F2F2F2;
}
.cf-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 22rpx 0;
border-bottom: 1rpx solid #F7F7F7;
}
.cf-row:last-child {
border-bottom: none;
}
.cf-k {
font-size: 26rpx;
color: #999;
flex-shrink: 0;
margin-right: 24rpx;
}
.cf-v {
font-size: 26rpx;
color: #222;
text-align: right;
flex: 1;
word-break: break-all;
}
/* 文件资料行 */
.cf-file-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 22rpx 0;
border-bottom: 1rpx solid #F7F7F7;
}
.cf-file-row:last-child {
border-bottom: none;
}
.cf-thumbs {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
justify-content: flex-end;
flex: 1;
margin-left: 24rpx;
}
.cf-thumb {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
border: 1rpx solid #EEE;
}
.cf-empty {
font-size: 24rpx;
color: #B7B7B7;
}
/* 协议勾选 */
.agreement {
display: flex;
align-items: center;
padding: 24rpx 24rpx 0;
}
.agreement-text {
font-size: 24rpx;
color: #666;
margin-left: 8rpx;
}
.agreement-link {
color: #FF370B;
}
/* 底部按钮(吸底) */
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #FFFFFF;
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
display: flex;
gap: 16rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
z-index: 99;
}
.btn-primary {
flex: 1;
height: 88rpx;
background: linear-gradient(91deg, #FF7658 0%, #FF370B 100%);
border-radius: 44rpx;
color: #FFFFFF;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-secondary {
flex: 1;
height: 88rpx;
background: #FFFFFF;
border: 1rpx solid #FF370B;
border-radius: 44rpx;
color: #FF370B;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,209 @@
<template>
<view class="container">
<!-- 顶部进度条 -->
<view class="step-bar">
<view class="step-pill active">填写信息</view>
<view class="step-pill">提交审核</view>
<view class="step-pill">入驻成功</view>
</view>
<view class="confirm-head">请确认以下信息</view>
<!-- 基本信息 -->
<view class="cf-group">
<view class="cf-group-title">基本信息</view>
<view class="cf-row"><text class="cf-k">门店名称</text><text class="cf-v">{{ d.merchant_name || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">所在地区</text><text class="cf-v">{{ d._region_label || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">详细地址</text><text class="cf-v">{{ d.address || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">商家类目</text><text class="cf-v">{{ d._cate_name || '—' }}</text></view>
<view class="cf-row" v-if="d._biz_status_label"><text class="cf-k">营业状态</text><text class="cf-v">{{ d._biz_status_label }}</text></view>
<view class="cf-row"><text class="cf-k">负责人姓名</text><text class="cf-v">{{ d.contact_name || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">负责人手机</text><text class="cf-v">{{ d.phone || '—' }}</text></view>
</view>
<!-- 资质信息 -->
<view class="cf-group">
<view class="cf-group-title">资质信息</view>
<view class="cf-row"><text class="cf-k">执照名称</text><text class="cf-v">{{ d.license_name || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">信用代码</text><text class="cf-v">{{ d.credit_code || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">营业执照</text><text class="cf-v">{{ d.license_is_forever === 1 ? '永久有效' : (d.license_valid_until || '—') }}</text></view>
<view class="cf-row" v-if="d.industry_name"><text class="cf-k">证书名称</text><text class="cf-v">{{ d.industry_name }}</text></view>
<view class="cf-row" v-if="d.industry_cert_no"><text class="cf-k">证书编号</text><text class="cf-v">{{ d.industry_cert_no }}</text></view>
<view class="cf-row" v-if="d.industry_name"><text class="cf-k">资质有效期</text><text class="cf-v">{{ d.industry_is_forever === 1 ? '永久有效' : (d.industry_valid_until || '') }}</text></view>
<view class="cf-row"><text class="cf-k">法人姓名</text><text class="cf-v">{{ d.legal_person || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">身份证号</text><text class="cf-v">{{ d.id_card_no || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">证件有效期</text><text class="cf-v">{{ d.id_is_forever === 1 ? '永久有效' : (d.id_valid_until || '—') }}</text></view>
</view>
<!-- 结算信息 -->
<view class="cf-group">
<view class="cf-group-title">结算信息</view>
<view class="cf-row"><text class="cf-k">银行开户名</text><text class="cf-v">{{ d.account_name || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">{{ d.account_type === 2 ? '对公账号' : '银行卡号' }}</text><text class="cf-v">{{ d.bank_card || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">开户银行</text><text class="cf-v">{{ d.bank_name || '—' }}</text></view>
<view class="cf-row"><text class="cf-k">开户支行</text><text class="cf-v">{{ d.bank_branch || '—' }}</text></view>
<view class="cf-row" v-if="d._bank_region_label"><text class="cf-k">银行所在地</text><text class="cf-v">{{ d._bank_region_label }}</text></view>
<view class="cf-row" v-if="d.bank_union_no"><text class="cf-k">联行号</text><text class="cf-v">{{ d.bank_union_no }}</text></view>
</view>
<!-- 文件资料 -->
<view class="cf-group">
<view class="cf-group-title">文件资料</view>
<view class="cf-file-row">
<text class="cf-k">营业执照</text>
<view class="cf-thumbs">
<image v-for="(u, i) in licenseUrls" :key="i" :src="u" class="cf-thumb" mode="aspectFill" @click="preview(licenseUrls, i)" />
<text v-if="!licenseUrls.length" class="cf-empty">未上传</text>
</view>
</view>
<view class="cf-file-row">
<text class="cf-k">行业资质</text>
<view class="cf-thumbs">
<image v-for="(u, i) in industryUrls" :key="i" :src="u" class="cf-thumb" mode="aspectFill" @click="preview(industryUrls, i)" />
<text v-if="!industryUrls.length" class="cf-empty">未上传</text>
</view>
</view>
<view class="cf-file-row">
<text class="cf-k">法人证件</text>
<view class="cf-thumbs">
<image v-for="(u, i) in idUrls" :key="i" :src="u" class="cf-thumb" mode="aspectFill" @click="preview(idUrls, i)" />
<text v-if="!idUrls.length" class="cf-empty">未上传</text>
</view>
</view>
<view class="cf-file-row">
<text class="cf-k">门头环境</text>
<view class="cf-thumbs">
<image v-for="(u, i) in facadeUrls" :key="i" :src="u" class="cf-thumb" mode="aspectFill" @click="preview(facadeUrls, i)" />
<text v-if="!facadeUrls.length" class="cf-empty">未上传</text>
</view>
</view>
<view class="cf-file-row">
<text class="cf-k">店内环境</text>
<view class="cf-thumbs">
<image v-for="(u, i) in interiorUrls" :key="i" :src="u" class="cf-thumb" mode="aspectFill" @click="preview(interiorUrls, i)" />
<text v-if="!interiorUrls.length" class="cf-empty">未上传</text>
</view>
</view>
</view>
<!-- 协议勾选 -->
<view class="agreement">
<view class="agreement-check" @click="agreeProtocol = !agreeProtocol">
<u-icon :name="agreeProtocol ? 'checkmark-circle-fill' : 'checkmark-circle'" :color="agreeProtocol ? '#FF370B' : '#ccc'" size="36"></u-icon>
</view>
<view class="agreement-text">
我已阅读并同意<text class="agreement-link" @click.stop="viewProtocol">商家入驻协议</text>
</view>
</view>
<view class="bottom-bar">
<view class="btn-secondary" @click="goBack">返回修改</view>
<view class="btn-primary" @click="submit">提交审核</view>
</view>
</view>
</template>
<script>
import { apiArr } from '../../../api/v2local';
import { request, picUrl, NavgateTo } from '../../../utils';
import { signPrivateView } from '../../../utils/uploadOSS.js';
export default {
data() {
return {
picUrl,
d: {},
agreeProtocol: false,
licenseUrls: [],
industryUrls: [],
idUrls: [],
facadeUrls: [],
interiorUrls: [],
submitting: false,
}
},
onLoad(options) {
if (options && options.itemObj) {
try {
this.d = JSON.parse(decodeURIComponent(options.itemObj))
} catch (e) {
console.error('解析确认页参数失败:', e)
this.d = {}
}
}
// / bucket URL
this.facadeUrls = (this.d.facade_photo || '').split(',').filter(Boolean).map(p => this.picUrl + p)
this.interiorUrls = (this.d.interior_photo || '').split(',').filter(Boolean).map(p => this.picUrl + p)
// / / bucket
this.signList(this.d.license_photo, 'licenseUrls')
this.signList(this.d.industry_photo, 'industryUrls')
this.signList([this.d.id_card_front, this.d.id_card_back].filter(Boolean).join(','), 'idUrls')
},
methods: {
async signList(paths, key) {
const list = (paths || '').split(',').filter(Boolean)
const result = []
for (const p of list) {
try {
const r = await signPrivateView(p)
result.push(r.url)
} catch (e) {
console.error('签发私密 URL 失败:', p, e)
}
}
this[key] = result
},
preview(urls, current) {
if (!urls || !urls.length) return
uni.previewImage({ urls, current: urls[current] })
},
viewProtocol() {
uni.showModal({
title: '商家入驻协议',
content: '本协议是您与平台之间关于商家入驻服务的法律协议。入驻后您需遵守平台规则,按时提供商品或服务,保证信息真实有效。平台有权对违规商家进行处罚。',
showCancel: false,
confirmText: '我知道了'
})
},
goBack() {
uni.navigateBack()
},
submit() {
if (!this.agreeProtocol) {
uni.showToast({ title: '请先阅读并同意商家入驻协议', icon: 'none' })
return
}
if (this.submitting) return
this.submitting = true
// 线
const params = {}
Object.keys(this.d).forEach(k => {
if (!k.startsWith('_')) params[k] = this.d[k]
})
request(apiArr.createStore, "POST", params).then(res => {
// reLaunch /
const auditItem = {
status: 1,
create_time: new Date().toLocaleString('sv-SE').replace('T', ' '),
handle_time: '',
remark: ''
}
uni.reLaunch({
url: "/packages/shopEnter/auditStatus/index?itemObj=" + encodeURIComponent(JSON.stringify(auditItem))
})
}).catch(res => {
this.submitting = false
const msg = res && res.message ? res.message : '提交失败,请稍后重试'
if (msg.includes("agent_nil")) {
uni.showModal({ title: '提交失败', content: '未找到对应的代理商信息,请联系平台', showCancel: false })
return
}
uni.showModal({ title: '提交失败', content: msg, showCancel: false })
})
}
}
}
</script>
<style>
@import url("./index.css");
</style>

View File

@ -0,0 +1,66 @@
page {
background-color: #f6f7fb;
min-height: 100vh;
}
.container {
padding: 30rpx;
}
.example-section {
background: #FFFFFF;
border-radius: 20rpx;
padding: 30rpx;
}
.title {
font-size: 34rpx;
font-weight: bold;
color: #222222;
margin-bottom: 30rpx;
text-align: center;
}
.sub-section {
margin-bottom: 40rpx;
}
.sub-title {
font-size: 28rpx;
color: #333333;
margin-bottom: 20rpx;
font-weight: bold;
}
.example-img {
width: 100%;
margin-bottom: 20rpx;
display: flex;
justify-content: center;
}
.example-img image {
width: 500rpx;
border-radius: 10rpx;
border: 2rpx dashed #ddd;
}
.tips {
background: #FFF8F6;
border-radius: 10rpx;
padding: 24rpx;
margin-top: 20rpx;
}
.tips-title {
font-size: 26rpx;
font-weight: bold;
color: #FF370B;
margin-bottom: 16rpx;
}
.tips-item {
font-size: 24rpx;
color: #666666;
line-height: 1.8;
}

View File

@ -0,0 +1,83 @@
<template>
<view class="container">
<view class="example-section" v-if="type === 'license'">
<view class="title">营业执照拍摄示例</view>
<view class="example-img">
<image :src="picUrl + '/static/example/license.jpg'" mode="widthFix"></image>
</view>
<view class="tips">
<view class="tips-title">拍摄要求</view>
<view class="tips-item">1. 请确保营业执照在有效期内</view>
<view class="tips-item">2. 照片需清晰完整四角完整可见</view>
<view class="tips-item">3. 不得有遮挡涂改反光</view>
<view class="tips-item">4. 支持原件拍照或彩色扫描件</view>
</view>
</view>
<view class="example-section" v-if="type === 'idcard'">
<view class="title">身份证拍摄示例</view>
<view class="sub-section">
<view class="sub-title">身份证正面人像面</view>
<view class="example-img">
<image :src="picUrl + '/static/example/front.jpg'" mode="widthFix"></image>
</view>
</view>
<view class="sub-section">
<view class="sub-title">身份证反面国徽面</view>
<view class="example-img">
<image :src="picUrl + '/static/example/back.jpg'" mode="widthFix"></image>
</view>
</view>
<view class="tips">
<view class="tips-title">拍摄要求</view>
<view class="tips-item">1. 请确保身份证在有效期内</view>
<view class="tips-item">2. 照片需清晰完整四角完整可见</view>
<view class="tips-item">3. 不得有遮挡涂改反光</view>
<view class="tips-item">4. 仅支持二代身份证</view>
</view>
</view>
<view class="example-section" v-if="type === 'industry'">
<view class="title">行业资质示例</view>
<view class="example-img">
<image src="/static/example/license.jpg" mode="widthFix"></image>
</view>
<view class="tips">
<view class="tips-title">拍摄要求</view>
<view class="tips-item">1. 请上传与经营品类对应的资质如食品经营许可证</view>
<view class="tips-item">2. 照片需清晰完整四角完整可见</view>
<view class="tips-item">3. 不得有遮挡涂改反光</view>
<view class="tips-item">4. 资质需在有效期内</view>
</view>
</view>
</view>
</template>
<script>
import { picUrl } from '../../../utils/index';
export default {
data() {
return {
picUrl,
type: 'license'
}
},
onLoad(options) {
if (options.type) {
this.type = options.type;
}
//
const titleMap = {
license: '营业执照示例',
idcard: '身份证示例',
industry: '行业资质示例'
}
uni.setNavigationBarTitle({
title: titleMap[this.type] || '资质示例'
})
}
}
</script>
<style>
@import url("./index.css");
</style>

View File

@ -1,116 +1,674 @@
/* ============================================================
* 商家入驻 - 基本信息 tab 全新样式按设计稿 PDF 1
* 主色 #FF370B 辅色 #FFF1ED 灰边 #EEE 灰字 #999 深字 #222
* ============================================================ */
page {
background-color: #f6f7fb;
min-height: 100vh;
padding-bottom: 60rpx;
background-color: #F6F7F9;
min-height: 100vh;
padding-bottom: 200rpx;
}
.container {
background: url(https://static.hshuishang.com/property-img-file/shopEn_apply.png) no-repeat;
background-size: 750rpx 497rpx;
box-sizing: border-box;
padding-top: 290rpx;
padding-bottom: 40rpx;
box-sizing: border-box;
}
/* ================= 顶部进度条(红底胶囊) ================= */
.step-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx 28rpx;
background: linear-gradient(91deg, #FF7658 0%, #FF370B 100%);
}
.step-pill {
flex: 1;
height: 60rpx;
margin: 0 8rpx;
border-radius: 30rpx;
background: rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.65);
font-size: 26rpx;
}
.step-pill.active {
background: #FFFFFF;
color: #FF370B;
font-weight: 600;
}
/* ================= Tab 切换 ================= */
.tab-bar {
display: flex;
background: #FFFFFF;
padding: 0 60rpx;
}
.tab-item {
flex: 1;
text-align: center;
padding: 28rpx 0 22rpx;
font-size: 30rpx;
color: #222;
position: relative;
}
.tab-item.active {
color: #FF370B;
font-weight: 600;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 8rpx;
left: 50%;
transform: translateX(-50%);
width: 56rpx;
height: 6rpx;
background: #FF370B;
border-radius: 3rpx;
}
/* ================= 表单卡片 ================= */
.form-card {
background: #FFFFFF;
margin: 20rpx 24rpx 0;
padding: 24rpx;
border-radius: 16rpx;
}
.field {
margin-top: 20rpx;
}
.field:first-child {
margin-top: 0;
}
.field-label {
font-size: 26rpx;
color: #222;
margin-bottom: 14rpx;
display: flex;
align-items: center;
}
.field-label .star {
color: #FF370B;
margin-right: 4rpx;
font-size: 26rpx;
}
.field-input {
height: 80rpx;
border: 1rpx solid #EBEBEB;
border-radius: 8rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
background: #FFFFFF;
}
.field-input input {
flex: 1;
font-size: 28rpx;
color: #222;
height: 80rpx;
line-height: 80rpx;
}
.field-input input::placeholder,
.field-input .placeholder {
color: #B7B7B7;
}
.field-input.disabled {
background: #FAFAFA;
}
.field-value {
flex: 1;
font-size: 28rpx;
color: #222;
}
.field-value.placeholder {
color: #B7B7B7;
}
.field-arrow {
color: #C8C8C8;
font-size: 24rpx;
margin-left: 12rpx;
}
/* ================= 上传图片块 ================= */
.upload-section {
margin-top: 20rpx;
}
.upload-title {
font-size: 26rpx;
color: #222;
margin-bottom: 16rpx;
display: flex;
align-items: center;
}
.upload-title .star {
color: #FF370B;
margin-right: 4rpx;
}
.upload-title .count {
color: #999;
font-size: 24rpx;
margin-left: 8rpx;
font-weight: normal;
}
.upload-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
/* uview u-upload 内部样式微调(让占位框跟设计稿一致) */
.upload-section /deep/ .u-upload__wrap__preview {
width: 168rpx !important;
height: 168rpx !important;
border-radius: 12rpx !important;
overflow: hidden;
}
.upload-section /deep/ .u-upload__button {
width: 168rpx !important;
height: 168rpx !important;
border-radius: 12rpx !important;
background: #FAFAFA !important;
border: 1rpx dashed #D1D1D1 !important;
}
.upload-add {
width: 168rpx;
height: 168rpx;
background: #FAFAFA;
border: 1rpx dashed #D1D1D1;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
font-size: 22rpx;
}
.upload-add-icon {
font-size: 48rpx;
color: #C8C8C8;
line-height: 1;
margin-bottom: 6rpx;
}
/* ================= 底部红色提示框(门店信息要求) ================= */
.tip-box {
margin: 20rpx 24rpx 0;
background: #FFF7F4;
border: 1rpx solid #FFD9CD;
border-radius: 12rpx;
padding: 20rpx 24rpx 24rpx;
display: flex;
gap: 12rpx;
}
.tip-box .tip-icon {
color: #FF370B;
font-size: 28rpx;
font-weight: bold;
flex-shrink: 0;
line-height: 40rpx;
}
.tip-box .tip-content {
flex: 1;
}
.tip-box .tip-title {
font-size: 26rpx;
color: #FF370B;
font-weight: 600;
margin-bottom: 8rpx;
}
.tip-box .tip-item {
font-size: 24rpx;
color: #FF370B;
line-height: 40rpx;
}
/* ================= 底部按钮(吸底) ================= */
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #FFFFFF;
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
display: flex;
gap: 16rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
z-index: 99;
}
.btn-primary {
flex: 1;
height: 88rpx;
background: linear-gradient(91deg, #FF7658 0%, #FF370B 100%);
border-radius: 44rpx;
color: #FFFFFF;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-secondary {
flex: 1;
height: 88rpx;
background: #FFFFFF;
border: 1rpx solid #FF370B;
border-radius: 44rpx;
color: #FF370B;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
/* ================= 协议勾选 ================= */
.agreement {
display: flex;
align-items: center;
padding: 24rpx 24rpx 0;
}
.agreement-text {
font-size: 24rpx;
color: #666;
margin-left: 8rpx;
}
.agreement-link {
color: #FF370B;
}
/* ================= 资质示例链接 ================= */
.example-link {
font-size: 24rpx;
color: #FF370B;
margin-top: 10rpx;
}
/* ================= 弹层选择器(所在地区/类目/营业状态) ================= */
.popup-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: flex-end;
}
.popup-sheet {
width: 100%;
background: #FFFFFF;
border-radius: 24rpx 24rpx 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #F0F0F0;
}
.popup-header .pop-cancel {
color: #999;
font-size: 28rpx;
}
.popup-header .pop-title {
color: #222;
font-size: 30rpx;
font-weight: 600;
}
.popup-header .pop-confirm {
color: #FF370B;
font-size: 28rpx;
}
.popup-body {
flex: 1;
display: flex;
overflow: hidden;
}
.popup-col {
flex: 1;
overflow-y: auto;
padding: 12rpx 0;
}
.popup-col-item {
padding: 24rpx 32rpx;
font-size: 28rpx;
color: #222;
display: flex;
align-items: center;
justify-content: space-between;
}
.popup-col-item.selected {
color: #FF370B;
}
.popup-col-item .check {
color: #FF370B;
font-size: 24rpx;
}
/* 单列选择列表(营业状态) */
.popup-list {
flex: 1;
overflow-y: auto;
}
.popup-list-item {
padding: 30rpx 32rpx;
font-size: 30rpx;
color: #222;
text-align: center;
border-bottom: 1rpx solid #F5F5F5;
}
.popup-list-item.selected {
color: #FF370B;
}
/* ================= 类目选择搜索框 ================= */
.popup-search {
margin: 16rpx 32rpx;
background: #F5F5F5;
border-radius: 8rpx;
padding: 14rpx 20rpx;
display: flex;
align-items: center;
gap: 12rpx;
font-size: 26rpx;
color: #B7B7B7;
}
/* ============ Tab2 资质相关 ============ */
.qual-thumb {
width: 100%;
height: 360rpx;
border-radius: 12rpx;
position: relative;
overflow: hidden;
border: 1rpx solid #EBEBEB;
}
.qual-thumb-img {
width: 100%;
height: 100%;
}
.qual-thumb-del {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 32rpx;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.id-card-row {
display: flex;
gap: 20rpx;
margin-top: 12rpx;
}
.id-card-cell {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.id-card-cell .upload-add {
width: 100%;
height: 200rpx;
}
.id-card-cell .qual-thumb {
width: 100%;
height: 200rpx;
}
.id-card-cell-label {
font-size: 24rpx;
color: #999;
}
.forever-toggle {
display: flex;
align-items: center;
gap: 10rpx;
margin-top: 12rpx;
padding-left: 4rpx;
}
.forever-toggle-text {
font-size: 24rpx;
color: #666;
}
/* ============ 有效期分段控件(永久有效 / 截止日期) ============ */
.validity-seg {
display: flex;
gap: 16rpx;
}
.validity-seg .seg-item {
flex: 1;
height: 72rpx;
border: 1rpx solid #EBEBEB;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #666;
background: #FFFFFF;
}
.validity-seg .seg-item.active {
color: #FF370B;
border-color: #FF370B;
background: #FFF1ED;
font-weight: 600;
}
/* ============ 结算页:银行账户信息分组标题 ============ */
.section-title {
display: flex;
align-items: center;
font-size: 30rpx;
color: #222;
font-weight: 600;
padding: 28rpx 24rpx 4rpx;
}
.section-title::before {
content: '';
width: 6rpx;
height: 30rpx;
background: #FF370B;
border-radius: 3rpx;
margin-right: 12rpx;
}
/* 结算页行内底部提示 */
.row-tip {
font-size: 22rpx;
color: #86909C;
padding: 12rpx 24rpx 0;
line-height: 34rpx;
}
/* 结算页可点击选择行右侧布局 */
.row_con--select {
display: flex;
align-items: center;
justify-content: space-between;
}
.row_con .row_value {
flex: 1;
font-size: 28rpx;
color: #222;
}
.row_con .row_value.placeholder {
color: #B7B7B7;
}
/* ============ 结算页 银行账户信息表单 ============ */
.Msg {
width: 710rpx;
background: #FFFFFF;
border-radius: 20rpx 20rpx 20rpx 20rpx;
margin: 0 auto;
padding: 0 20rpx;
box-sizing: border-box;
background: #FFFFFF;
margin: 12rpx 24rpx 0;
border-radius: 16rpx;
padding: 0 24rpx;
}
.row {
padding-top: 30rpx;
display: flex;
justify-content: space-between;
.Msg .row {
display: flex;
flex-direction: column;
padding: 24rpx 0;
border-bottom: 1rpx solid #F2F2F2;
}
.row_label {
font-size: 28rpx;
color: #999999;
width: 180rpx;
.Msg .row:last-child {
border-bottom: none;
}
.red {
color: #FF370B;
margin-left: 5rpx;
.Msg .row_label {
font-size: 26rpx;
color: #222;
margin-bottom: 14rpx;
display: flex;
align-items: center;
}
.row_con {
flex: 1;
padding-bottom: 30rpx;
border-bottom: 1rpx solid #EBEBEB;
display: flex;
.Msg .row_label .star {
color: #FF370B;
margin-right: 4rpx;
}
.row_con input {
flex: 1;
.Msg .row_con {
height: 80rpx;
border: 1rpx solid #EBEBEB;
border-radius: 8rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
background: #FFFFFF;
}
.nonebor {
border-bottom: none;
.Msg .row_con input {
flex: 1;
font-size: 28rpx;
color: #222;
height: 80rpx;
line-height: 80rpx;
}
.row2 {
display: flex;
align-items: center;
.Msg .row_con input::placeholder {
color: #B7B7B7;
}
.mt {
margin-top: 26rpx;
padding-top: 36rpx;
padding-bottom: 36rpx;
/* 结算页底部按钮组 */
.btn-group {
display: flex;
gap: 16rpx;
padding: 40rpx 24rpx;
}
.imgCon {
font-size: 18rpx;
color: #222222;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 120rpx;
height: 120rpx;
background: #F6F7FB;
border: 1rpx solid #D1D1D1;
border-radius: 10rpx 10rpx 10rpx 10rpx;
.btn-group .prevBtn {
flex: 1;
height: 88rpx;
background: #FFFFFF;
border: 1rpx solid #FF370B;
border-radius: 44rpx;
color: #FF370B;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
.imgCon image {
width: 34rpx;
height: 34rpx;
margin-bottom: 8rpx;
.btn-group .nextBtn {
flex: 1;
height: 88rpx;
background: linear-gradient(91deg, #FF7658 0%, #FF370B 100%);
border-radius: 44rpx;
color: #FFFFFF;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
.mt2 {
margin-top: 30rpx;
.action-sheet {
width: 100%;
background: #F2F2F4;
padding-bottom: env(safe-area-inset-bottom);
}
.u-upload__wrap__preview {
width: 120rpx;
height: 120rpx;
border-radius: 10rpx 0rpx 10rpx 10rpx !important;
.action-item {
background: #FFFFFF;
padding: 32rpx 0;
text-align: center;
font-size: 32rpx;
color: #222;
border-bottom: 1rpx solid #F0F0F0;
}
.u-upload__wrap__preview__image {
width: 100% !important;
height: 100% !important;
object-fit: cover;
.action-item:active {
background: #F8F8F8;
}
.addBtn {
font-size: 36rpx;
color: #FFFFFF;
width: 600rpx;
height: 90rpx;
background: linear-gradient(91deg, #FF7658 0%, #FF370B 100%);
border-radius: 100rpx 100rpx 100rpx 100rpx;
margin: 0 auto;
margin-top: 58rpx;
display: flex;
align-items: center;
justify-content: center;
.action-cancel {
margin-top: 12rpx;
color: #999;
border-bottom: none;
}

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@ page {
.button {
width: 400rpx;
height: 90rpx;
background: #ff7252;
background: #FF0101;
color: white;
display: flex;
justify-content: center;

View File

@ -53,6 +53,12 @@ page {
font-size: 36rpx;
color: #ffffff;
}
.logout-btn {
margin-top: 40rpx;
background: #ffffff;
border: 1rpx solid #ff370b;
color: #ff370b;
}
.tabel1 {
margin: 0 20rpx;
border-bottom: 1rpx solid #ebebeb;

View File

@ -73,6 +73,7 @@
</view>
</view>
</view>
<view class="btn logout-btn" @click="headerLogoutClick">退出登录</view>
<nav-footer :current="4"/>
</view>
</template>
@ -191,6 +192,36 @@ export default {
});
},
// 退
headerLogoutClick() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (!res.confirm) return;
//
const keys = [
'ctoken', 'userId', 'openId', 'phone', 'is_deal', 'is_dev',
'is_shop', 'is_merchant', 'is_worker', 'shopId',
'order_dispatch_permission', 'work_order_permission',
'nickName', 'headPhoto', 'changeCommData', 'currentRoomNo'
];
keys.forEach(k => uni.removeStorageSync(k));
uni.showToast({
title: '已退出登录',
icon: 'success',
mask: true,
duration: 1500,
success: () => {
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' });
}, 1500);
}
});
}
});
},
headerRadioClick(item) {
this.sex = item;
},

View File

@ -5,6 +5,7 @@
"style": {
"navigationStyle": "custom",
"navigationBarBackgroundColor": "#FFF",
"enablePullDownRefresh": true,
"usingComponents": {
"nav-footer": "/components/nav/nav"
}
@ -133,6 +134,47 @@
}
]
},
{
"root": "packages/homeService",
"pages": [
{
"path": "index/index",
"style": { "navigationBarTitleText": "到家服务", "navigationBarBackgroundColor": "#fff" }
},
{
"path": "detail/index",
"style": { "navigationBarTitleText": "服务详情", "navigationBarBackgroundColor": "#fff" }
},
{
"path": "order/index",
"style": { "navigationBarTitleText": "确认预约", "navigationBarBackgroundColor": "#fff" }
},
{
"path": "myOrders/index",
"style": { "navigationBarTitleText": "我的服务订单", "navigationBarBackgroundColor": "#fff" }
},
{
"path": "orderDetail/index",
"style": { "navigationBarTitleText": "订单详情", "navigationBarBackgroundColor": "#fff" }
},
{
"path": "worker/index",
"style": { "navigationBarTitleText": "我的接单", "navigationBarBackgroundColor": "#fff" }
},
{
"path": "myContracts/index",
"style": { "navigationBarTitleText": "我的家政合同", "navigationBarBackgroundColor": "#fff" }
},
{
"path": "contractDetail/index",
"style": { "navigationBarTitleText": "合同详情", "navigationBarBackgroundColor": "#fff" }
},
{
"path": "myMonthlyBills/index",
"style": { "navigationBarTitleText": "我的月账单", "navigationBarBackgroundColor": "#fff" }
}
]
},
{
"root": "packages/customerService",
"pages": [
@ -144,6 +186,13 @@
"navigationBarBackgroundColor": "#F9F9F9"
}
},
{
"path": "picker/index",
"style": {
"navigationBarTitleText": "选择",
"navigationBarBackgroundColor": "#fff"
}
},
{
"path": "chattingRecords/index",
"style": {
@ -571,6 +620,13 @@
{
"root": "packages/shopEnter",
"pages": [
{
"path": "choose/index",
"style": {
"navigationBarTitleText": "商家入驻",
"navigationBarBackgroundColor": "#fff"
}
},
{
"path": "index/index",
"style": {
@ -578,6 +634,20 @@
"navigationBarBackgroundColor": "#fff"
}
},
{
"path": "confirm/index",
"style": {
"navigationBarTitleText": "商家入驻",
"navigationBarBackgroundColor": "#fff"
}
},
{
"path": "camera/index",
"style": {
"navigationBarTitleText": "拍摄证件",
"navigationStyle": "custom"
}
},
{
"path": "sucess/index",
"style": {
@ -589,7 +659,14 @@
{
"path": "auditStatus/index",
"style": {
"navigationBarTitleText": "审核状态",
"navigationBarTitleText": "资料审核",
"navigationBarBackgroundColor": "#fff"
}
},
{
"path": "example/index",
"style": {
"navigationBarTitleText": "资质示例",
"navigationBarBackgroundColor": "#fff"
}
}
@ -602,7 +679,8 @@
"path": "index/index",
"style": {
"navigationBarTitleText": "湖畔好店",
"navigationBarBackgroundColor": "#fff"
"navigationBarBackgroundColor": "#fff",
"enablePullDownRefresh": true
}
},
{

View File

@ -203,7 +203,7 @@
买单返积分
</view>
</view>
<view class="merchantItem_right_con_right" @click="toJump(item)">
<view class="merchantItem_right_con_right" @click.stop="toJump(item)">
<image src="https://static.hshuishang.com/property-img-file/local_review.png" mode="aspectFill">
</image>
点评
@ -489,6 +489,9 @@ export default {
uni.setStorageSync('location', preciseLocation);
uni.setStorageSync('ad_code', ad_info.adcode);
// TODO: ad_code
// 广1-11 getHomeXxx
// banner-region
await this.prefetchBannerGroups();
const [bannerList, serverLeft, serverRightList, homeLeftList, homeRightList, bottomList, buttonList, categoryList] = await Promise.all([
this.getHomeBanner(),//
@ -665,30 +668,57 @@ export default {
async headershopEnterClick() {
if (!uni.getStorageSync('userId')) {
NavgateTo('/packages/shopEnter/index/index', { isLogin: false });
NavgateTo('/packages/shopEnter/choose/index', { isLogin: false });
return
}
const res = await request(apiArr2.statusQuery, "POST", {}, { silent: false });
if (res.status) {
NavgateTo('/packages/shopEnter/auditStatus/index?itemObj=' + JSON.stringify(res));
} else {
NavgateTo('/packages/shopEnter/index/index');
NavgateTo('/packages/shopEnter/choose/index');
}
},
async getHomeBanner() {
// 广 this._bannerGroups getHomeXxx
// banner-region
async prefetchBannerGroups(positions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) {
try {
const res = await request(apiArr2.getHomeBannerMulti, "POST", {
ad_code: Number(uni.getStorageSync('ad_code')),
ad_positions: positions,
longitude: uni.getStorageSync('location').lng,
latitude: uni.getStorageSync('location').lat,
page_size: 50
}, { silent: false });
this._bannerGroups = (res && res.groups) ? res.groups : {};
} catch (e) {
console.warn('批量拉取首页广告失败,回退逐个请求:', e);
this._bannerGroups = null; // 退
}
},
// 广退 page
async getBannerRows(position) {
if (this._bannerGroups) {
const rows = this._bannerGroups[String(position)] || [];
return rows;
}
const res = await request(apiArr2.getHomeBanner, "POST", {
ad_code: uni.getStorageSync('ad_code'),
ad_position: 1,
ad_position: position,
longitude: uni.getStorageSync('location').lng,
latitude: uni.getStorageSync('location').lat,
page_num: 1,
page_size: 10
page_size: 50
}, { silent: false });
return (res && res.rows) ? res.rows : [];
},
if (res.rows && res.rows.length) {
let filterRes = this.filterShowList(res.rows, 1);
async getHomeBanner() {
const rows = await this.getBannerRows(1);
if (rows && rows.length) {
let filterRes = this.filterShowList(rows, 1);
filterRes.forEach(item => {
item.pic_src = picUrl + item.pic_src
})
@ -699,21 +729,14 @@ export default {
},
async getServerLeft() {
const res = await request(apiArr2.getHomeBanner, "POST", {
ad_code: uni.getStorageSync('ad_code'),
ad_position: 2,
longitude: uni.getStorageSync('location').lng,
latitude: uni.getStorageSync('location').lat,
page_num: 1,
page_size: 10
}, { silent: false });
if (!res.rows || !res.rows.length) {
const rows = await this.getBannerRows(2);
if (!rows || !rows.length) {
this.serverLeftList = []
}
if (res.rows && res.rows.length) {
this.serverLeftList = res.rows
let filterRes = this.filterShowList(res?.rows, 1);
if (rows && rows.length) {
this.serverLeftList = rows
let filterRes = this.filterShowList(rows, 1);
filterRes.forEach(item => {
item.pic_src = picUrl + item.pic_src
})
@ -727,19 +750,12 @@ export default {
async getServerRight() {
const rightList = []
for (let i = 3; i < 5; i++) {
const res = await request(apiArr2.getHomeBanner, "POST", {
ad_code: uni.getStorageSync('ad_code'),
ad_position: i,
longitude: uni.getStorageSync('location').lng,
latitude: uni.getStorageSync('location').lat,
page_num: 1,
page_size: 10
}, { silent: false });
if (!res.rows || !res.rows.length) {
const rows = await this.getBannerRows(i);
if (!rows || !rows.length) {
this.serverRightList = []
}
if (res.rows && res.rows.length) {
let filterRes = this.filterShowList(res?.rows, 1);
if (rows && rows.length) {
let filterRes = this.filterShowList(rows, 1);
filterRes.forEach(item => {
item.pic_src = picUrl + item.pic_src
})
@ -750,16 +766,9 @@ export default {
},
async getHomeMidLeft() {
const res = await request(apiArr2.getHomeBanner, "POST", {
ad_code: uni.getStorageSync('ad_code'),
ad_position: 5,
longitude: uni.getStorageSync('location').lng,
latitude: uni.getStorageSync('location').lat,
page_num: 1,
page_size: 10
}, { silent: false });
if (res.rows && res.rows.length) {
let filterRes = this.filterShowList(res?.rows, 1);
const rows = await this.getBannerRows(5);
if (rows && rows.length) {
let filterRes = this.filterShowList(rows, 1);
filterRes.forEach(item => {
item.pic_src = picUrl + item.pic_src
})
@ -772,19 +781,9 @@ export default {
async getHomeMidRight() {
const rightList = []
for (let i = 6; i < 12; i++) {
const res = await request(apiArr2.getHomeBanner, "POST", {
ad_code: uni.getStorageSync('ad_code'),
ad_position: i,
longitude: uni.getStorageSync('location').lng,
latitude: uni.getStorageSync('location').lat,
page_num: 1,
page_size: 10
}, { silent: false });
if (res.rows && res.rows.length) {
// let firstItem = res.rows[0];
// firstItem.pic_src = picUrl + firstItem.pic_src;
// rightList.push(firstItem);
let filterRes = this.filterShowList(res?.rows, 1);
const rows = await this.getBannerRows(i);
if (rows && rows.length) {
let filterRes = this.filterShowList(rows, 1);
filterRes.forEach(item => {
item.pic_src = picUrl + item.pic_src
})
@ -1197,6 +1196,14 @@ export default {
}
},
onPullDownRefresh() {
this.bottomList = [];
this.bottomPageNum = 1;
this.flag = false;
this.init();
uni.stopPullDownRefresh();
},
async onReachBottom() {
if (this.flag) {
const res = await this.getHomeBottom();

View File

@ -17,6 +17,16 @@
<view class="login-tip" style="font-size: 28rpx; color: #555555; margin-top: 30rpx;">为了更好的体验申请获取您的公开信息
</view>
<view class="login-tip" style="font-size: 28rpx; color: #555555; margin-top: 14rpx;">头像昵称等</view>
<view class="profile-fill">
<button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image class="avatar-img" :src="avatarUrl || defaultAvatar" mode="aspectFill"></image>
<text class="avatar-tip">点击选择头像</text>
</button>
<input type="nickname" class="nickname-input" :value="nickName" placeholder="点击填写昵称"
@blur="onNickInput" @input="onNickInput" />
</view>
<view class="login-button">
<button class="login-btn" open-type="getPhoneNumber" @click="headerLoginClick"
@getphonenumber="getPhoneNumber">
@ -37,6 +47,7 @@
import { apiArr } from '../../api/login';
import { apiArr as apiArr2 } from '../../api/v2User';
import { request } from '../../utils';
import { uploadOSS } from '@/utils/uploadOSS';
export default {
data() {
return {
@ -44,10 +55,26 @@ export default {
loginChannel: '', //
userId: '',
passWord: '',
nickName: '', // ()
avatarUrl: '', // (OSS)
avatarTemp: '', // OSS
defaultAvatar: 'https://static.hshuishang.com/loginMainImg.png',
}
},
methods: {
// ( token)
// get-phone token OSS
onChooseAvatar(e) {
this.avatarTemp = e.detail.avatarUrl || '';
this.avatarUrl = this.avatarTemp; //
},
//
onNickInput(e) {
this.nickName = e.detail.value || '';
},
headerInputClick(e) {
const { name } = e.currentTarget.dataset;
const { value } = e.detail;
@ -114,12 +141,30 @@ export default {
// return
if (event.detail.errMsg === "getPhoneNumber:ok") {
request(apiArr.loginGetUserPhone, 'POST', {
code: event.detail.code
}, { silent: false }).then((res) => {
code: event.detail.code,
user_id: uni.getStorageSync('userId'),
nick_name: this.nickName || ''
}, { silent: false }).then(async (res) => {
console.log(res);
uni.hideLoading();
if (isLogin) {
uni.setStorageSync('phone', res.phone);
if (this.nickName) uni.setStorageSync('nickName', this.nickName);
// ( token) OSS
if (this.avatarTemp) {
try {
const { objectKey } = await uploadOSS({ filePath: this.avatarTemp, scene: 'avatar', showLoading: false });
// object key(/) CDN
const avatarPath = '/' + objectKey;
await request(apiArr2.userInfoUpdate, 'POST', {
user_id: uni.getStorageSync('userId'),
avatar: avatarPath
}, { silent: true });
uni.setStorageSync('headPhoto', avatarPath);
} catch (err) {
console.error('头像上传/保存失败:', err);
}
}
uni.hideLoading();
uni.showToast({
title: '登录成功',
icon: 'success',
@ -132,6 +177,7 @@ export default {
}
});
} else {
uni.hideLoading();
uni.showToast({
title: '登录失败',
icon: 'error',
@ -181,8 +227,10 @@ export default {
page_num: 1,
page_size: 10,
}, { silent: false }).then((res3) => {
const { rows: [{}] } = res3;
uni.setStorageSync("changeCommData",{ name: rows[0].name, id: rows[0].community_id })
const rows = (res3 && res3.rows) || [];
if (rows.length > 0) {
uni.setStorageSync("changeCommData", { name: rows[0].name, id: rows[0].community_id });
}
})
},
@ -260,4 +308,49 @@ export default {
<style>
@import url("./login.css");
.profile-fill {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 30rpx;
}
.avatar-btn {
display: flex;
flex-direction: column;
align-items: center;
background: transparent;
border: none;
padding: 0;
line-height: normal;
}
.avatar-btn::after {
border: none;
}
.avatar-img {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: #f2f2f2;
}
.avatar-tip {
font-size: 24rpx;
color: #999999;
margin-top: 10rpx;
}
.nickname-input {
width: 420rpx;
height: 80rpx;
margin-top: 24rpx;
padding: 0 24rpx;
border: 1rpx solid #e0e0e0;
border-radius: 12rpx;
font-size: 28rpx;
text-align: center;
}
</style>

View File

@ -71,11 +71,11 @@
src="https://static.hshuishang.com/property-img-file/page_user_Group_1557.png" />
<text class="device-title title-item">我的工单</text>
</view>
<view class="item_device" @click="handleMeApplyClick">
<!--view class="item_device" @click="handleMeApplyClick">
<image class="icon-img"
src="https://static.hshuishang.com/property-img-file/page_user_Group_1558.png" />
<text class="device-title title-item">我的管家</text>
</view>
</view-->
</view>
</view>
</view>
@ -169,16 +169,16 @@ export default {
name: "团购订单",
url: "/packages/myOrders/groupOrders/index",
},
{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1565.png",
name: "我的收藏",
url: "",
},
{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1566.png",
name: "售后服务",
url: "",
},
// {
// image: "https://static.hshuishang.com/property-img-file/page_user_Group_1565.png",
// name: "",
// url: "",
// },
// {
// image: "https://static.hshuishang.com/property-img-file/page_user_Group_1566.png",
// name: "",
// url: "",
// },
{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1563.png",
name: "收货地址",
@ -192,34 +192,34 @@ export default {
],
visitList: [{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1572.png",
name: "服务工单",
url: "",
name: "到家服务订单",
url: "/packages/homeService/myOrders/index",
},
{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1573.png",
name: "服务地址",
url: "",
name: "我的月账单",
url: "/packages/homeService/myMonthlyBills/index",
},
{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1574.png",
name: "服务卡",
url: "",
name: "我的家政合同",
url: "/packages/homeService/myContracts/index",
}
],
shopList: [{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1580.png",
name: "附近门店",
url: "",
},
{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1581.png",
name: "服务券",
url: "",
url: "/packages/localLife/index/index",
},
// {
// image: "https://static.hshuishang.com/property-img-file/page_user_Group_1581.png",
// name: "",
// url: "",
// },
{
image: "https://static.hshuishang.com/property-img-file/page_user_Group_1582.png",
name: "支付记录",
url: "",
url: "/packages/localLife/payInfo/index",
}
],
shopManagementList: [
@ -240,6 +240,12 @@ export default {
name: "物业端",
url: "/packages/workOrderDashboard/guide/index",
isShow: uni.getStorageSync("is_worker"),
},
{
image: "https://static.hshuishang.com/property-img-file/userPageWuye.png",
name: "师傅端",
url: "/packages/homeService/worker/index",
isShow: uni.getStorageSync("is_merchant"),
}
],
parkList: [
@ -347,6 +353,12 @@ export default {
name: "物业端",
url: "/packages/workOrderDashboard/guide/index",
isShow: loginRes.is_worker,
},
{
image: "https://static.hshuishang.com/property-img-file/userPageWuye.png",
name: "师傅端",
url: "/packages/homeService/worker/index",
isShow: loginRes.is_merchant,
}
];

View File

@ -1,7 +1,7 @@
{
"appid": "wx1addb25675dd8e70",
"compileType": "miniprogram",
"libVersion": "3.8.3",
"libVersion": "3.16.0",
"packOptions": {
"ignore": [],
"include": []
@ -19,7 +19,18 @@
"disablePlugins": [],
"outputPath": ""
},
"condition": true
"condition": true,
"compileWorklet": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"swc": false,
"disableSWC": true
},
"condition": {},
"editorSetting": {

View File

@ -2,7 +2,22 @@
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "uniapp-ZHSQ",
"setting": {
"compileHotReLoad": true
"compileHotReLoad": true,
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"showShadowRootInWxmlPanel": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false
},
"libVersion": "3.9.1"
"libVersion": "3.16.0",
"condition": {}
}

View File

@ -3,8 +3,8 @@ const environments = {
development: {
apiUrl: "https://test.hshuishang.com",
picUrl: "https://test.hshuishang.com",
aliyunOssUrl: "https://wechat-img-file.oss-cn-beijing.aliyuncs.com",
staticUrl: "https://static.hshuishang.com",
aliyunOssUrl: "https://wechat-img-file-dev.oss-cn-beijing.aliyuncs.com",
staticUrl: "https://static-dev.hshuishang.com",
},
production: {
apiUrl: "https://api.hshuishang.com",
@ -16,11 +16,6 @@ const environments = {
// 判断当前环境
const getCurrentEnvironment = () => {
// 1. 优先通过NODE_ENV判断
if (process && process.env && process.env.NODE_ENV) {
return process.env.NODE_ENV;
}
// 2. 微信小程序环境判断
if (typeof wx !== "undefined" && wx.getAccountInfoSync) {
try {
@ -39,6 +34,11 @@ const getCurrentEnvironment = () => {
}
}
// 1. 优先通过NODE_ENV判断
if (process && process.env && process.env.NODE_ENV) {
return process.env.NODE_ENV;
}
// 3. 通过全局配置判断例如Vercel等平台的环境变量
if (typeof global !== "undefined" && global.env) {
return global.env;
@ -58,9 +58,10 @@ const currentEnv = getCurrentEnvironment();
const envConfig = environments[currentEnv] || environments.production;
export const RequsetUrl = envConfig.apiUrl; // 请求地址前缀
export const picUrl = envConfig.picUrl; // 图片地址前缀
export const aliyunOssUrl = envConfig.aliyunOssUrl; // 阿里云OSS地址
export const staticUrl = envConfig.staticUrl; // 静态资源地址
// 公开图片域名:统一走 CDNdev: static-dev.hshuishang.com / prod: static.hshuishang.com
export const picUrl = envConfig.staticUrl;
export const aliyunOssUrl = envConfig.aliyunOssUrl; // 阿里云OSS直链地址私密文件签名 URL 已自带 host一般不需要直接拼
export const staticUrl = envConfig.staticUrl; // CDN 加速域名
/**
* 处理图片URL根据环境自动替换
@ -226,7 +227,7 @@ export const request = (
}
resolve(res.data.data); // 请求成功
} else {
console.log("走到这列");
console.log("走到这列",RequsetUrl, url,res.header,res.data,res.profile);
uni.hideLoading();
// uni.showToast({
// title: res.data.msg || "请求失败",
@ -325,70 +326,26 @@ export const floatCalculate = (num1, num2, operator) => {
};
/**
* 图片上传
* @param {string} filename - 图片上传地址
* @param {Function} fn - 接口回调函数
* 图片上传 OSS 直传回调形状对齐旧后端 { code, data: { path } }path / 开头
* @param {string} filename - 本地文件路径
* @param {Function} fn - 回调函数
* @param {string} [scene] - 场景默认 common
*/
export const upload = (filename, fn) => {
uni.showLoading({
title: "上传中",
mask: true,
});
uni.uploadFile({
url: RequsetUrl + "/api/v1/public/upload-image",
filePath: filename,
name: "image",
formData: {
uid: uni.getStorageSync("uid"),
},
success: (f) => {
uni.hideLoading();
fn(JSON.parse(f.data));
},
fail: (res) => {
uni.hideLoading();
console.log(res);
uni.showToast({
title: "上传文件失败",
icon: "none",
});
},
complete: () => {},
});
export const upload = (filename, fn, scene = "common") => {
// 引入此处避免顶层循环依赖
const { uploadOSSCompat } = require("./uploadOSS.js");
uploadOSSCompat(filename, fn, scene);
};
/**
* 视频上传
* @param {string} filename - 图片上传地址
* @param {Function} fn - 接口回调函数
* 视频上传 OSS 直传
* @param {string} filename - 本地文件路径
* @param {Function} fn - 回调函数
* @param {string} [scene] - 场景默认 video
*/
export const uploadVideo = (filename, fn) => {
uni.showLoading({
title: "上传中",
mask: true,
});
uni.uploadFile({
url: RequsetUrl + "/api/v1/public/upload-video",
filePath: filename,
name: "file",
formData: {
uid: uni.getStorageSync("uid"),
},
success: (f) => {
uni.hideLoading();
fn(JSON.parse(f.data));
},
fail: (res) => {
uni.hideLoading();
console.log(res);
uni.showToast({
title: "上传文件失败",
icon: "none",
});
},
complete: () => {},
});
export const uploadVideo = (filename, fn, scene = "video") => {
const { uploadOSSCompat } = require("./uploadOSS.js");
uploadOSSCompat(filename, fn, scene);
};
/**

152
utils/uploadOSS.js Normal file
View File

@ -0,0 +1,152 @@
// uniapp-ZHSQ —— OSS 直传客户端
//
// 用法:
// import { uploadOSS } from '@/utils/uploadOSS'
// const { objectKey, url } = await uploadOSS({ filePath: tempFilePath, scene: 'avatar' })
// // objectKey: OSS 上的相对 key存数据库
// // url: 公开访问 URL如果走签名 URL 显示,前端再单独处理)
//
// 支持的 scene 由服务端 wechatOssSceneCfg 控制,目前:
// - avatar 用户头像 (jpg/jpeg/png/webp<=5MB)
// - merchant 商家入驻资料 (jpg/jpeg/png/webp/pdf<=10MB)
import { RequsetUrl, aliyunOssUrl } from "@/utils/index.js";
const inferExt = (filePath) => {
if (!filePath) return "";
const idx = filePath.lastIndexOf(".");
if (idx < 0) return "";
return filePath.substring(idx + 1).toLowerCase();
};
const fetchCredential = (scene, ext, bindId) => {
return new Promise((resolve, reject) => {
const data = { scene, ext };
if (bindId) data.bind_id = bindId; // 客服聊天场景:按会话分目录
uni.request({
url: RequsetUrl + "/api/v1/wechat/oss/credential",
method: "POST",
header: {
Authorization: uni.getStorageSync("ctoken") || "",
"Content-Type": "application/json",
},
data,
success: (res) => {
if (res.statusCode !== 200) return reject(new Error("获取上传凭证失败:" + res.statusCode));
const body = res.data || {};
// 兼容服务端通用响应包装
const data = body.data || body;
if (!data || !data.host) return reject(new Error("上传凭证字段缺失"));
resolve(data);
},
fail: (err) => reject(err),
});
});
};
const postToOSS = (filePath, cred) => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: cred.host,
filePath,
name: "file", // 必须是 fileOSS PostObject 协议约定
formData: {
key: cred.object_key,
OSSAccessKeyId: cred.access_key_id,
"x-oss-security-token": cred.security_token,
policy: cred.policy,
signature: cred.signature,
success_action_status: "200",
},
success: (res) => {
// OSS 200 -> 空 body 视为成功
if (res.statusCode === 200 || res.statusCode === 204) {
resolve();
} else {
reject(new Error("OSS 上传失败:" + res.statusCode + " " + (res.data || "")));
}
},
fail: (err) => reject(err),
});
});
};
/**
* 直传 OSS
* @param {Object} opts
* @param {string} opts.filePath 本地文件临时路径uni.chooseImage 拿到的 path
* @param {string} opts.scene 上传场景avatar / merchant
* @param {string} [opts.ext] 扩展名不含点不传则从 filePath 推断
* @param {boolean} [opts.showLoading=true]
* @returns {Promise<{ objectKey: string, url: string }>}
*/
export const uploadOSS = async ({ filePath, scene, ext, bindId, showLoading = true }) => {
if (!filePath) throw new Error("filePath 不能为空");
if (!scene) throw new Error("scene 不能为空");
const finalExt = (ext || inferExt(filePath)).toLowerCase();
if (!finalExt) throw new Error("无法识别文件扩展名");
if (showLoading) uni.showLoading({ title: "上传中", mask: true });
try {
const cred = await fetchCredential(scene, finalExt, bindId);
await postToOSS(filePath, cred);
return {
objectKey: cred.object_key,
url: aliyunOssUrl + "/" + cred.object_key,
};
} finally {
if (showLoading) uni.hideLoading();
}
};
/**
* 兼容旧版 utils/index.js upload(filename, fn) 的回调式 API
* 内部走 OSS 直传回调 res 形状对齐旧版res.data.path 为以 / 开头的 object key
* 调用方 picUrl + res.data.path 可以直接拼成完整 URL
*
* @param {string} filePath
* @param {Function} fn 接收 { code, msg, data: { path } }
* @param {string} [scene='merchant']
*/
export const uploadOSSCompat = (filePath, fn, scene = "merchant") => {
uploadOSS({ filePath, scene })
.then(({ objectKey }) => {
fn && fn({ code: 1, msg: "ok", data: { path: "/" + objectKey } });
})
.catch((err) => {
console.error("uploadOSSCompat 失败:", err);
uni.showToast({ title: err.message || "上传文件失败", icon: "none" });
});
};
/**
* 取私密文件查看 URL小程序商家本人查看自己上传的证件 / 合同
* 服务端会校验 path 必须以 wechat/merchant_private/{当前 user_id}/ 开头
*
* @param {string} objectKey 存数据库的 path带或不带前导 /
* @returns {Promise<{ url: string, expireSeconds: number }>}
*/
export const signPrivateView = (objectKey) => {
return new Promise((resolve, reject) => {
if (!objectKey) return reject(new Error("object_key 不能为空"));
const cleanKey = objectKey.replace(/^\/+/, "");
uni.request({
url: RequsetUrl + "/api/v1/wechat/oss/sign-private-view",
method: "POST",
header: {
Authorization: uni.getStorageSync("ctoken") || "",
"Content-Type": "application/json",
},
data: { object_key: cleanKey },
success: (res) => {
if (res.statusCode !== 200) return reject(new Error("签发失败:" + res.statusCode));
const body = res.data || {};
const data = body.data || body;
if (!data || !data.url) return reject(new Error("签发结果字段缺失"));
resolve({ url: data.url, expireSeconds: data.expire_seconds });
},
fail: (err) => reject(err),
});
});
};