feat:完善小程序

main
wk 2026-03-09 11:37:41 +08:00
parent 075054a6c1
commit 1941fd60e5
23 changed files with 1334 additions and 113 deletions

20
App.vue
View File

@ -4,10 +4,26 @@
export default {
globalData: {
//
serviceCategory: null
serviceCategory: null,
// scene-id '0-123'
inviteCode: null
},
onLaunch: function() {
onLaunch: function(options) {
console.log('App Launch')
// scene -id 0-1231-123
const scene = (options && options.query && options.query.scene) || (options && options.scene)
const sceneStr = scene != null ? String(scene) : ''
if (sceneStr && /^\d+-\d+$/.test(sceneStr)) {
this.globalData.inviteCode = sceneStr
uni.setStorageSync('inviteCode', sceneStr)
// 便
if (!uni.getStorageSync('token')) {
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' })
}, 50)
return
}
}
//
this.checkLoginStatus()
},

View File

@ -27,7 +27,7 @@ export function login(data) {
* @param {String} data.phoneCode 手机 code, 小程序通过 wx.getPhoneNumber 方法获得
* @param {String} data.loginCode 登录 code, 小程序通过 wx.login 方法获得
* @param {String} data.state state 参数必填用于回调的随机值
* @param {String} data.inviteCode 邀请码可选第一位是类型第二位是用户id例如1-1, 0-1
* @param {String} data.inviteCode 邀请码可选格式 "邀请类型-用户id"0=会员邀请会员1=系统用户邀请会员例如 0-1231-123
* @returns {Promise} 返回登录结果包含token等
*/
export function loginByPhone(data) {

View File

@ -28,6 +28,17 @@ export function createRealNameInfo(data) {
})
}
// 获得实名信息
export function getRealNameInfo(data) {
return request({
url: '/app-api/member/user-real-name/get',
method: 'GET',
data: data,
showLoading: false,
needAuth: true // 需要token认证
})
}
// 我的收藏
export function getMyCollect(data) {
return request({
@ -39,6 +50,17 @@ export function getMyCollect(data) {
})
}
// 业务数据获取,主要是分享,收藏等根据业务id 和类型获取数据
export function getBusinessData(data) {
return request({
url: '/app-api/member/lu-buiness/get',
method: 'GET',
data: data,
showLoading: false,
needAuth: true // 需要token认证
})
}
// 创建会员建议
export function createLaborUnionSuggest(data) {
return request({
@ -50,6 +72,17 @@ export function createLaborUnionSuggest(data) {
})
}
// 获得会员建议分页
export function getLaborUnionSuggestPage(data) {
return request({
url: '/app-api/member/labor-union-suggest/page',
method: 'GET',
data: data,
showLoading: false,
needAuth: true // 需要token认证
})
}
// 发布消息
export function createLaborUnionMessage(data) {
return request({
@ -60,3 +93,43 @@ export function createLaborUnionMessage(data) {
needAuth: true // 需要token认证
})
}
/**
* 获取邀请小程序码图片入参格式与后端约定一致支持返回 URL Base64
* @param {String} inviteCode 邀请码格式 邀请类型-用户id '0-123'会作为 scene 传给后端若后端需数字可传 userId
* @returns {Promise<String>} 小程序码图片 URL Data URLBase64失败则抛出
*/
export function getInviteQRCode(inviteCode) {
// 入参示例scene、path、width、autoColor、checkPath、hyaline
const scene = inviteCode || ''
const path = 'pages/login/login'
return request({
url: '/app-api/member/social-user/wxa-qrcode',
method: 'POST',
data: {
scene,
path,
width: 430,
autoColor: true,
checkPath: true,
hyaline: true
},
showLoading: false,
needAuth: true
}).then(res => {
if (res == null) return null
// 接口可能直接返回 base64 字符串request 里 resolve 的 data 即为该字符串)
if (typeof res === 'string') {
const raw = res.trim()
if (raw.startsWith('data:image')) return raw
if (raw.length > 0) return 'data:image/png;base64,' + raw
return null
}
const url = res.url || (res.data && res.data.url)
if (url) return url
const base64 = res.data && typeof res.data === 'string' ? res.data : (res.data && res.data.data)
if (typeof base64 === 'string' && base64.length > 0)
return base64.startsWith('data:image') ? base64 : 'data:image/png;base64,' + base64
return null
})
}

View File

@ -6,8 +6,13 @@
</view> -->
<view class="action-item" @click="handleLike">
<image class="action-icon" src="/static/service/like_icon.png" mode="aspectFill"></image>
<text class="action-text">点赞</text>
<image
class="action-icon"
:class="{ active: isLiked }"
:src="isLiked ? '/static/service/like_icon_active.png' : '/static/service/like_icon.png'"
mode="aspectFill"
></image>
<text class="action-text" :class="{ active: isLiked }">点赞</text>
</view>
<button class="action-item share-button" open-type="share" @click="handleShareClick">
@ -16,8 +21,13 @@
</button>
<view class="action-item" @click="handleCollect">
<image class="action-icon" src="/static/service/collect_icon.png" mode="aspectFill"></image>
<text class="action-text">收藏</text>
<image
class="action-icon"
:class="{ active: isCollected }"
:src="isCollected ? '/static/service/collect_icon_active.png' : '/static/service/collect_icon.png'"
mode="aspectFill"
></image>
<text class="action-text" :class="{ active: isCollected }">收藏</text>
</view>
</view>
</template>
@ -255,11 +265,6 @@ export default {
&.active {
transform: scale(1.1);
}
.icon-text {
font-size: 48rpx;
line-height: 1;
}
}
.action-text {

View File

@ -1,10 +1,18 @@
<template>
<view class="complaints-page">
<!-- 头部区域 -->
<NavHeader title="投诉建议" />
<!-- 表单内容区 -->
<scroll-view class="form-content" scroll-y="true">
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="投诉建议" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<scroll-view
class="form-content"
scroll-y="true"
:refresher-enabled="true"
:refresher-triggered="listRefreshing"
@refresherrefresh="onListRefresh"
@scrolltolower="onListLoadMore"
:lower-threshold="80"
>
<!-- 投诉建议表单 -->
<view class="form-section">
<view class="section-title">填写投诉建议</view>
@ -52,12 +60,41 @@
{{ loading ? '提交中...' : '提交' }}
</button>
</view>
<!-- 我的投诉建议列表 -->
<view class="list-section">
<view class="section-title">我的投诉建议</view>
<view class="empty-list" v-if="!listLoading && suggestList.length === 0">
<text class="empty-text">暂无投诉建议记录</text>
</view>
<view
class="suggest-card"
v-for="(item, index) in suggestList"
:key="item.id || index"
>
<view class="card-header">
<text class="card-title">{{ item.title || '无标题' }}</text>
<text class="card-time">{{ formatTime(item.createTime) }}</text>
</view>
<text class="card-content" v-if="item.content">{{ item.content }}</text>
<view class="card-reply" v-if="getReplyText(item)">
<text class="reply-label">{{ item.replyName }}回复</text>
<text class="reply-text">{{ getReplyText(item) }}</text>
</view>
</view>
<view class="list-footer" v-if="suggestList.length > 0">
<text v-if="listLoadMore" class="footer-text">...</text>
<text v-else-if="!listHasMore" class="footer-text">没有更多了</text>
<text v-else class="footer-text">上拉加载更多</text>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { createLaborUnionSuggest } from '@/api/profile.js'
import { createLaborUnionSuggest,getLaborUnionSuggestPage } from '@/api/profile.js'
import NavHeader from "@/components/NavHeader/NavHeader.vue";
export default {
@ -66,22 +103,91 @@ export default {
},
data() {
return {
statusBarHeight: 0,
loading: false,
formData: {
title: '',
content: ''
}
},
suggestList: [],
listPageNo: 1,
listPageSize: 10,
listHasMore: true,
listLoading: false,
listLoadMore: false,
listRefreshing: false
}
},
computed: {
headerHeight() {
return this.statusBarHeight + 44
},
//
canSubmit() {
return this.formData.title && this.formData.title.trim().length >= 1
}
},
onLoad() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
this.loadSuggestList()
},
methods: {
formatTime(val) {
if (val == null || val === '') return ''
let ts = Number(val)
if (!Number.isFinite(ts)) return val
if (String(ts).length <= 10) ts *= 1000
const d = new Date(ts)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const h = String(d.getHours()).padStart(2, '0')
const min = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${day} ${h}:${min}`
},
getReplyText(item) {
const text = item.replyContent
return text ? String(text).trim() : ''
},
getReplyTime(item) {
const t = item.replyTime
return t != null && t !== '' ? this.formatTime(t) : ''
},
async loadSuggestList(append = false) {
if (this.listLoading) return
this.listLoading = true
try {
const res = await getLaborUnionSuggestPage({
pageNo: this.listPageNo,
pageSize: this.listPageSize
})
const raw = res.list || res.records || (res.data && (res.data.list || res.data.records)) || []
const list = Array.isArray(raw) ? raw : []
this.suggestList = append ? this.suggestList.concat(list) : list
this.listHasMore = list.length >= this.listPageSize
} catch (e) {
console.error('加载投诉建议列表失败:', e)
if (!append) this.suggestList = []
uni.showToast({ title: '加载列表失败', icon: 'none' })
} finally {
this.listLoading = false
this.listLoadMore = false
this.listRefreshing = false
}
},
onListRefresh() {
this.listRefreshing = true
this.listPageNo = 1
this.listHasMore = true
this.loadSuggestList()
},
onListLoadMore() {
if (!this.listHasMore || this.listLoadMore || this.listLoading) return
this.listLoadMore = true
this.listPageNo += 1
this.loadSuggestList(true)
},
//
async handleSubmit() {
//
@ -110,10 +216,11 @@ export default {
icon: 'success'
})
//
setTimeout(() => {
uni.navigateBack()
}, 1500)
this.formData.title = ''
this.formData.content = ''
this.listPageNo = 1
this.listHasMore = true
this.loadSuggestList()
} catch (error) {
console.error('提交投诉建议失败:', error)
uni.showToast({
@ -136,6 +243,22 @@ export default {
flex-direction: column;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #e2e8f1;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.form-content {
flex: 1;
height: 0;
@ -235,7 +358,7 @@ export default {
.submit-section {
margin-top: 40rpx;
padding-bottom: 40rpx;
padding-bottom: 24rpx;
.submit-btn {
width: 100%;
@ -260,4 +383,99 @@ export default {
}
}
}
.list-section {
margin-top: 24rpx;
padding-bottom: 40rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 20rpx;
padding-left: 10rpx;
}
.empty-list {
padding: 60rpx 0;
text-align: center;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
.suggest-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12rpx;
.card-title {
flex: 1;
font-size: 28rpx;
font-weight: 600;
color: #1a1819;
}
.card-time {
font-size: 22rpx;
color: #999999;
flex-shrink: 0;
margin-left: 16rpx;
}
}
.card-content {
display: block;
font-size: 26rpx;
color: #666666;
line-height: 1.5;
margin-bottom: 16rpx;
}
.card-reply {
background: #f0f6ff;
border-radius: 12rpx;
padding: 16rpx;
border-left: 4rpx solid #004294;
.reply-label {
font-size: 24rpx;
font-weight: 500;
color: #004294;
margin-right: 8rpx;
}
.reply-text {
font-size: 26rpx;
color: #333333;
line-height: 1.5;
}
.reply-time {
display: block;
font-size: 22rpx;
color: #999999;
margin-top: 8rpx;
}
}
}
.list-footer {
padding: 24rpx 0;
text-align: center;
.footer-text {
font-size: 24rpx;
color: #999999;
}
}
}
</style>

View File

@ -1,9 +1,9 @@
<template>
<view class="activities-list-page">
<!-- 顶部导航栏 -->
<NavHeader title="工会活动" :show-placeholder="true" />
<!-- 活动列表 -->
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="工会活动" :show-placeholder="true" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<scroll-view
class="activities-scroll"
scroll-y="true"
@ -61,6 +61,7 @@
</view>
</view>
</scroll-view>
</view>
</view>
</template>
@ -75,6 +76,7 @@ export default {
},
data() {
return {
statusBarHeight: 0,
activitiesList: [],
//
page: 1,
@ -86,7 +88,14 @@ export default {
refreshing: false,
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
},
onLoad() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
this.loadActivities();
},
methods: {
@ -185,7 +194,22 @@ export default {
overflow: hidden;
}
/* 活动列表 */
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #e2e8f1;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.activities-scroll {
flex: 1;
height: 0; // flex: 1 使 scroll-view

View File

@ -1,9 +1,9 @@
<template>
<view class="my-collect-page">
<!-- 顶部导航栏 -->
<NavHeader title="我的收藏" />
<!-- 收藏列表 -->
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="我的收藏" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<scroll-view
class="collect-scroll"
scroll-y="true"
@ -57,11 +57,12 @@
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { getMyCollect } from "@/api/profile.js";
import { getMyCollect,getBusinessData } from "@/api/profile.js";
import NavHeader from "@/components/NavHeader/NavHeader.vue";
export default {
@ -70,6 +71,7 @@ export default {
},
data() {
return {
statusBarHeight: 0,
collectList: [],
//
pageNo: 1,
@ -81,7 +83,14 @@ export default {
refreshing: false,
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
},
onLoad() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
this.loadCollectList();
},
methods: {
@ -160,11 +169,17 @@ export default {
return `${year}-${month}-${day} ${hours}:${minutes}`;
},
//
// getBusinessData(id: collectId, type: collectType)
handleCollectClick(item) {
// uni.navigateTo({
// url: `/pages/detail/richTextDetail?id=${item.collectId}`,
// });
const collectId = item.collectId ?? item.id;
const collectType = item.collectType ?? item.type ?? "";
if (collectId == null || collectId === "") {
uni.showToast({ title: "数据异常", icon: "none" });
return;
}
uni.navigateTo({
url: `/pages/detail/richTextDetail?collectId=${encodeURIComponent(collectId)}&collectType=${encodeURIComponent(collectType)}&hideBar=1`,
});
},
},
};
@ -179,7 +194,22 @@ export default {
overflow: hidden;
}
/* 收藏列表 */
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #e2e8f1;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.collect-scroll {
flex: 1;
height: 0; // flex: 1 使 scroll-view

View File

@ -1,9 +1,12 @@
<template>
<view class="post-message-page">
<!-- 头部区域 -->
<NavHeader title="发布消息" />
<!-- 头部区域固定滑动时不动 -->
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="发布消息" />
</view>
<!-- 表单内容区 -->
<!-- 表单内容区预留头部高度 -->
<view class="form-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<scroll-view class="form-content" scroll-y="true">
<!-- 基本信息 -->
<view class="form-section">
@ -191,6 +194,7 @@
</button>
</view>
</scroll-view>
</view>
</view>
</template>
@ -204,12 +208,14 @@ export default {
},
data() {
return {
statusBarHeight: 0,
loading: false,
messageTypeIndex: -1,
messageTypeOptions: [
{ label: '类型1', value: 1 },
{ label: '类型2', value: 2 },
{ label: '类型3', value: 3 }
{ label: '租房', value: 3 },
{ label: '保险', value: 4 },
{ label: '健康', value: 5 },
{ label: '安全知识', value: 6 },
],
gradeIndex: -1,
gradeOptions: [
@ -230,6 +236,10 @@ export default {
}
},
computed: {
// + 88rpx 44px
headerHeight() {
return this.statusBarHeight + 44
},
// ISO
startTimeDisplay() {
if (!this.formData.startTime) return ''
@ -242,6 +252,8 @@ export default {
}
},
onLoad() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
},
methods: {
//
@ -415,6 +427,22 @@ export default {
flex-direction: column;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #e2e8f1;
}
.form-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.form-content {
flex: 1;
height: 0;

View File

@ -1,8 +1,9 @@
<template>
<view class="map-detail-page">
<!-- 顶部导航栏 -->
<NavHeader title="店铺位置" />
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="店铺位置" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<!-- 地图区域 -->
<view class="map-container" v-if="mapInitialized">
<map
@ -85,6 +86,7 @@
<view class="loading-mask" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
</view>
</view>
</template>
@ -99,6 +101,7 @@ export default {
},
data() {
return {
statusBarHeight: 0,
shopId: null,
storeInfo: {},
loading: false,
@ -118,6 +121,9 @@ export default {
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
// //
// distanceText() {
// if (
@ -162,6 +168,8 @@ export default {
},
},
onLoad(options) {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
this.shopId = options.id;
if (this.shopId) {
//
@ -421,6 +429,22 @@ export default {
overflow: hidden;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #f5f5f5;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* 地图容器 */
.map-container {
width: 100%;

View File

@ -1,9 +1,9 @@
<template>
<view class="rich-text-detail-page">
<!-- 顶部导航栏 -->
<NavHeader title="详情" />
<!-- 内容区域 -->
<view class="rich-text-detail-page" :class="{ 'no-bottom-bar': hideBottomBar }">
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="详情" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<scroll-view class="content-scroll" scroll-y="true">
<!-- 加载中 -->
<view class="loading-state" v-if="loading">
@ -45,20 +45,22 @@
</view>
</scroll-view>
<!-- 底部操作栏 -->
<!-- 底部操作栏从收藏进入时隐藏 -->
<DetailActionBar
v-if="detailData"
v-if="detailData && !hideBottomBar"
:liked="isLiked"
:collected="isCollected"
:id="detailId"
:zanType="noticeType"
:type="noticeType"
/>
</view>
</view>
</template>
<script>
import { getGuildDetail } from "@/api/home.js";
import { getBusinessData } from "@/api/profile.js";
import NavHeader from "@/components/NavHeader/NavHeader.vue";
import DetailActionBar from "@/components/DetailActionBar/DetailActionBar.vue";
@ -69,6 +71,7 @@ export default {
},
data() {
return {
statusBarHeight: 0,
detailId: null, // ID
title: "",
content: "",
@ -78,13 +81,28 @@ export default {
isCollected: false, //
detailData: null, // 使
noticeType: null, //
hideBottomBar: false, // true
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
},
onLoad(options) {
//
// 1. id activitiesDetail
// 2. title content richTextDetail
if (options.id) {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
//
// 1. collectId + collectType getBusinessData
// 2. id activitiesDetail
// 3. title content richTextDetail
const collectId = options.collectId != null ? decodeURIComponent(String(options.collectId)) : null;
const collectType = options.collectType != null ? decodeURIComponent(String(options.collectType)) : null;
if (collectId != null && collectId !== "") {
this.detailId = collectId;
this.hideBottomBar = !!(options.hideBar || options.hideBar === "1");
this.loadDetailByBusinessData(collectId, collectType || "");
} else if (options.id) {
this.detailId = options.id;
this.loadDetailById();
} else if (options.content) {
@ -148,6 +166,35 @@ export default {
},
// #endif
methods: {
// VO /
hasRespData(vo) {
if (vo == null) return false;
if (Array.isArray(vo)) return vo.length > 0;
if (typeof vo === "object") return Object.keys(vo).length > 0;
return !!vo;
},
// getBusinessData(id: collectId, type: collectType)
async loadDetailByBusinessData(collectId, collectType) {
try {
this.loading = true;
const res = await getBusinessData({ id: collectId, type: collectType });
if (res) {
this.detailData = res;
this.noticeType = res.noticeType ?? res.messageType ?? res.type ?? collectType;
this.title = res.title || "详情";
if (res.content) {
this.parsedContent = this.parseHtmlContent(res.content);
}
this.isLiked = this.hasRespData(res.zanRespVO);
this.isCollected = this.hasRespData(res.collectRespVO);
}
} catch (error) {
console.error("加载详情失败:", error);
uni.showToast({ title: "加载失败,请重试", icon: "none" });
} finally {
this.loading = false;
}
},
// ID activitiesDetail
async loadDetailById() {
if (!this.detailId) {
@ -165,13 +212,10 @@ export default {
if (res.content) {
this.parsedContent = this.parseHtmlContent(res.content);
}
//
if (res.isLiked !== undefined) {
this.isLiked = res.isLiked;
}
if (res.isCollected !== undefined) {
this.isCollected = res.isCollected;
}
// zanRespVO
this.isLiked = this.hasRespData(res.zanRespVO);
// collectRespVO
this.isCollected = this.hasRespData(res.collectRespVO);
}
} catch (error) {
console.error("加载详情失败:", error);
@ -254,9 +298,28 @@ export default {
flex-direction: column;
overflow: hidden;
padding-bottom: 120rpx; //
&.no-bottom-bar {
padding-bottom: 0;
}
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #e2e8f1;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* 内容区域 */
.content-scroll {
flex: 1;
height: 0; // flex: 1 使 scroll-view

View File

@ -1,8 +1,9 @@
<template>
<view class="store-page">
<!-- 顶部导航栏 -->
<NavHeader title="店铺详情" />
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="店铺详情" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<!-- 顶部店铺信息点击进入地图页 -->
<view class="store-header" @click="handleGoMap">
<view class="store-brand">
@ -112,6 +113,7 @@
去结算
</button>
</view>
</view>
</view>
</template>
@ -130,6 +132,7 @@ export default {
},
data() {
return {
statusBarHeight: 0,
storeInfo: {},
menuList: [],
categoryLabel: "",
@ -140,6 +143,9 @@ export default {
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
// 0
totalAmount() {
return this.menuList.reduce((total, item) => {
@ -164,7 +170,8 @@ export default {
},
},
onLoad(options) {
console.log(options, 111110);
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
// ID
this.shopId = options.id;
this.categoryLabel = options.categoryLabel;
@ -445,6 +452,22 @@ export default {
padding-bottom: 120rpx; //
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #e2e8f1;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* 顶部店铺信息 */
.store-header {
background-color: #ffffff;

View File

@ -1,10 +1,15 @@
<template>
<view class="home-page">
<!-- 应用头部 -->
<view class="header" :style="{ paddingTop: statusBarHeight + 'px' }">
<!-- 应用头部固定滑动时不动 -->
<view
class="header header-fixed"
:style="{ paddingTop: statusBarHeight + 'px' }"
>
<image class="logo" src="/static/home/logo.png" mode="aspectFit"></image>
</view>
<!-- 主体内容预留头部高度避免被遮挡 -->
<view class="main-content" :style="{ paddingTop: headerHeight + 'px' }">
<!-- 招募宣传横幅 -->
<swiper
class="recruit-banner"
@ -101,6 +106,7 @@
</view>
</view>
</view>
</view>
</view>
</template>
@ -121,6 +127,8 @@ export default {
statusBarHeight: 0,
// +
navBarHeight: 0,
// padding-top 72rpx 36px
headerContentHeight: 44,
bannerList: [],
//
benefitsList: [
@ -153,6 +161,11 @@ export default {
activitiesList: [],
};
},
computed: {
headerHeight() {
return this.statusBarHeight + this.headerContentHeight;
},
},
onLoad() {
this.getSystemInfo();
this.loadHomeData();
@ -384,6 +397,15 @@ export default {
width: 306rpx;
height: 72rpx;
}
&.header-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #e2e8f1;
}
}
/* 招募宣传横幅 */

View File

@ -1,11 +1,9 @@
<template>
<view class="login-page">
<!-- 状态栏占位 -->
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
<NavHeader title="登录" />
<!-- 登录内容区 -->
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="登录" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<view class="login-content">
<!-- Logo区域 -->
<view class="logo-section">
@ -93,6 +91,7 @@
</view>
</view>
</view>
</view>
</view>
</template>
@ -118,6 +117,9 @@ export default {
}
},
computed: {
headerHeight() {
return this.statusBarHeight + 44
},
//
canLogin() {
return (
@ -142,13 +144,17 @@ export default {
return
}
// state inviteCode
// stateinviteCode scene
if (options.state) {
this.state = options.state
}
if (options.inviteCode) {
this.inviteCode = options.inviteCode
}
if (options.scene && /^\d+-\d+$/.test(String(options.scene))) {
this.inviteCode = String(options.scene)
uni.setStorageSync('inviteCode', this.inviteCode)
}
//
const pages = getCurrentPages()
@ -492,6 +498,22 @@ export default {
background: linear-gradient(180deg, #e2e8f1 0%, #ffffff 100%);
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: linear-gradient(180deg, #e2e8f1 0%, #ffffff 100%);
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.status-bar {
background-color: transparent;
}

View File

@ -1,9 +1,15 @@
<template>
<view class="profile-page">
<view class="header" :style="{ paddingTop: statusBarHeight + 'px' }">
<!-- 头部固定滑动时不动 -->
<view
class="header header-fixed"
:style="{ paddingTop: statusBarHeight + 'px' }"
>
<text class="header-text">个人中心</text>
</view>
<!-- 主体内容预留头部高度 -->
<view class="profile-main" :style="{ paddingTop: navBarHeight + 'px' }">
<!-- 用户资料卡片 -->
<view
class="user-card"
@ -57,6 +63,9 @@
<text class="user-id">NO.{{ userInfo.id }}</text>
</view>
</view>
<view class="invite-btn" @click="goToInvite">
<image src="https://resource2.ctshenglong.cn/20260309/yaoqingma_btn_1773020094853.png" mode="aspectFill"></image>
</view>
<view class="member-benefits-btn" @click="goToMemberBenefits">
<image :src="memberLevelIcon" mode="aspectFill"></image>
</view>
@ -117,11 +126,40 @@
<view class="logout-section">
<button class="logout-btn" @click="handleLogout">退</button>
</view>
</view>
<!-- 邀请海报弹窗图5样式 -->
<view class="invite-modal-mask" v-if="inviteModalVisible" @click="closeInviteModal"></view>
<view class="invite-modal-wrap" v-if="inviteModalVisible" @click.stop>
<view class="invite-poster" id="invite-poster">
<view class="invite-poster-close" @click="closeInviteModal">×</view>
<view class="poster-inner">
<view class="poster-tag">邀请好友</view>
<view class="poster-qr-wrap" v-if="inviteQRCodeUrl">
<view class="poster-qr-outer">
<image class="poster-qr-image" :src="inviteQRCodeUrl" mode="aspectFit"></image>
</view>
<text class="poster-qr-tip">长按识别图中二维码</text>
</view>
</view>
</view>
<view class="poster-actions">
<view class="poster-btn poster-btn-cancel" @click="closeInviteModal"></view>
<view class="poster-btn poster-btn-save" @click="savePosterImage"></view>
</view>
<!-- 用于导出海报的 canvas隐藏 -->
<canvas
class="poster-canvas"
type="2d"
id="poster-canvas"
:style="{ width: posterCanvasWidth + 'px', height: posterCanvasHeight + 'px' }"
></canvas>
</view>
</view>
</template>
<script>
import { getUserInfo } from "@/api/profile.js";
import { getUserInfo, getInviteQRCode } from "@/api/profile.js";
export default {
data() {
@ -134,6 +172,12 @@ export default {
},
noticeNum: 3,
uploading: false, //
inviteModalVisible: false,
inviteQRCodeUrl: "",
inviteCodeStr: "", // -id 0-123
posterCanvasWidth: 680,
posterCanvasHeight: 860,
posterSaving: false,
};
},
computed: {
@ -234,6 +278,185 @@ export default {
}
}
},
// "-id" /app-api/member/auth/weixin-mini-app-login 使 inviteCode
// 0=1= 0-id
async goToInvite() {
const uid = this.userInfo && this.userInfo.id;
if (!uid) {
uni.showToast({ title: "请先登录", icon: "none" });
return;
}
const inviteCodeStr = `0-${uid}`;
this.inviteCodeStr = inviteCodeStr;
this.inviteModalVisible = true;
this.inviteQRCodeUrl = "";
try {
const url = await getInviteQRCode(inviteCodeStr);
if (url) {
this.inviteQRCodeUrl = url;
}
} catch (e) {
console.error("获取邀请二维码失败:", e);
}
},
closeInviteModal() {
this.inviteModalVisible = false;
},
// 5 canvas
async savePosterImage() {
if (this.posterSaving || !this.inviteQRCodeUrl) return;
this.posterSaving = true;
uni.showLoading({ title: "生成中...", mask: true });
const dpr = uni.getSystemInfoSync().pixelRatio || 2;
const w = this.posterCanvasWidth;
const h = this.posterCanvasHeight;
const padding = 48;
const qrSize = 260;
const qrTop = 220;
const avatarSize = 68;
try {
const query = uni.createSelectorQuery().in(this);
query
.select("#poster-canvas")
.fields({ node: true, size: true })
.exec(async (res) => {
if (!res || !res[0] || !res[0].node) {
uni.hideLoading();
this.posterSaving = false;
uni.showToast({ title: "生成失败,请重试", icon: "none" });
return;
}
const canvas = res[0].node;
const ctx = canvas.getContext("2d");
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.scale(dpr, dpr);
//
ctx.fillStyle = "#ffffff";
const r = 16;
ctx.beginPath();
ctx.moveTo(padding + r, padding);
ctx.lineTo(w - padding - r, padding);
ctx.quadraticCurveTo(w - padding, padding, w - padding, padding + r);
ctx.lineTo(w - padding, h - padding - r);
ctx.quadraticCurveTo(w - padding, h - padding, w - padding - r, h - padding);
ctx.lineTo(padding + r, h - padding);
ctx.quadraticCurveTo(padding, h - padding, padding, h - padding - r);
ctx.lineTo(padding, padding + r);
ctx.quadraticCurveTo(padding, padding, padding + r, padding);
ctx.closePath();
ctx.fill();
//
ctx.fillStyle = "#333333";
ctx.font = "bold 14px PingFang SC, sans-serif";
ctx.textAlign = "center";
const nickname = (this.userInfo.nickname || "微信用户").slice(0, 20);
ctx.font = "bold 16px PingFang SC, sans-serif";
ctx.fillText(nickname, w / 2, 96);
ctx.font = "13px PingFang SC, sans-serif";
ctx.fillStyle = "#666666";
ctx.fillText("长按识别图中二维码", w / 2, qrTop + qrSize + 44);
const drawImage = (src) => {
return new Promise((resolve, reject) => {
const img = canvas.createImage();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("图片加载失败"));
img.src = src;
});
};
try {
const qrImg = await drawImage(this.inviteQRCodeUrl);
const qrX = (w - qrSize) / 2;
ctx.save();
ctx.beginPath();
ctx.arc(w / 2, qrTop + qrSize / 2, qrSize / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
ctx.drawImage(qrImg, qrX, qrTop, qrSize, qrSize);
ctx.restore();
const avatarSrc = this.avatarSrc;
const avatarImg = await drawImage(avatarSrc);
const ax = (w - avatarSize) / 2;
const ay = qrTop + (qrSize - avatarSize) / 2;
ctx.save();
ctx.beginPath();
ctx.arc(w / 2, qrTop + qrSize / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
ctx.drawImage(avatarImg, ax, ay, avatarSize, avatarSize);
ctx.restore();
} catch (imgErr) {
console.warn("海报图片绘制跳过:", imgErr);
}
setTimeout(() => {
uni.createSelectorQuery()
.in(this)
.select("#poster-canvas")
.fields({ node: true, size: true })
.exec((r2) => {
if (!r2 || !r2[0] || !r2[0].node) {
uni.hideLoading();
this.posterSaving = false;
uni.showToast({ title: "导出失败", icon: "none" });
return;
}
const node = r2[0].node;
uni.canvasToTempFilePath({
canvas: node,
fileType: "png",
quality: 1,
success: (pathRes) => {
uni.saveImageToPhotosAlbum({
filePath: pathRes.tempFilePath,
success: () => {
uni.hideLoading();
this.posterSaving = false;
uni.showToast({ title: "已保存到相册", icon: "success" });
},
fail: (err) => {
uni.hideLoading();
this.posterSaving = false;
if (err.errMsg && err.errMsg.indexOf("auth") !== -1) {
uni.showModal({
title: "提示",
content: "需要您授权保存图片到相册",
confirmText: "去设置",
success: (s) => {
if (s.confirm) uni.openSetting();
},
});
} else {
uni.showToast({ title: "保存失败", icon: "none" });
}
},
});
},
fail: () => {
uni.hideLoading();
this.posterSaving = false;
uni.showToast({ title: "导出失败", icon: "none" });
},
});
});
}, 400);
});
} catch (e) {
console.error("保存海报失败:", e);
uni.hideLoading();
this.posterSaving = false;
uni.showToast({ title: "生成失败,请重试", icon: "none" });
}
},
//
goToMemberBenefits() {
uni.navigateTo({
@ -458,6 +681,15 @@ export default {
height: 88rpx;
line-height: 88rpx;
}
&.header-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: radial-gradient(0% 0% at 0% 0%, #ffffff 0%, #e2e8f1 100%);
}
}
/* 用户资料卡片 */
@ -565,6 +797,18 @@ export default {
}
}
.invite-btn {
position: absolute;
right: 255rpx;
bottom: 24rpx;
width: 175rpx;
height: 42rpx;
image {
width: 100%;
height: 100%;
}
}
.member-benefits-btn {
position: absolute;
right: 50rpx;
@ -702,5 +946,153 @@ export default {
align-items: center;
justify-content: center;
}
/* 邀请海报弹窗图5样式 */
.invite-modal-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 900;
}
.invite-modal-wrap {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 901;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx;
box-sizing: border-box;
}
.invite-poster {
width: 100%;
max-width: 660rpx;
background: #ffffff;
border-radius: 24rpx;
overflow: visible;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
position: relative;
}
.invite-poster-close {
position: absolute;
right: 20rpx;
top: 20rpx;
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 44rpx;
color: #999999;
z-index: 2;
}
.poster-inner {
padding: 48rpx 48rpx 44rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.poster-tag {
position: absolute;
top: 28rpx;
right: 96rpx;
padding: 8rpx 20rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #666666;
font-family: PingFang-SC, PingFang-SC;
}
.poster-nickname {
font-size: 38rpx;
font-weight: bold;
font-family: PingFang-SC, PingFang-SC;
color: #333333;
margin-bottom: 40rpx;
}
.poster-qr-wrap {
display: flex;
flex-direction: column;
align-items: center;
}
.poster-qr-outer {
width: 380rpx;
height: 380rpx;
position: relative;
border-radius: 50%;
overflow: hidden;
background: #f5f5f5;
}
.poster-qr-image {
width: 100%;
height: 100%;
border-radius: 50%;
}
.poster-qr-avatar-wrap {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 120rpx;
height: 120rpx;
border-radius: 50%;
overflow: hidden;
background: #fff;
padding: 8rpx;
box-sizing: border-box;
}
.poster-qr-avatar {
width: 100%;
height: 100%;
border-radius: 50%;
}
.poster-qr-tip {
font-size: 26rpx;
color: #999999;
margin-top: 28rpx;
font-family: PingFang-SC, PingFang-SC;
}
.poster-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 28rpx;
margin-top: 32rpx;
width: 100%;
max-width: 660rpx;
}
.poster-btn {
flex: 1;
max-width: 280rpx;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-family: PingFang-SC, PingFang-SC;
}
.poster-btn-cancel {
background: #f0f0f0;
color: #666666;
}
.poster-btn-save {
background: #004294;
color: #ffffff;
font-weight: 600;
}
.poster-canvas {
position: fixed;
left: -9999px;
top: 0;
z-index: -1;
}
</style>

View File

@ -1,7 +1,9 @@
<template>
<view class="rules-page">
<NavHeader title="积分会员制度" />
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="积分会员制度" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<scroll-view class="content-scroll" scroll-y="true">
<!-- 会员等级与积分门槛 -->
<view class="section">
@ -67,6 +69,7 @@
</view>
</view>
</scroll-view>
</view>
</view>
</template>
@ -77,6 +80,7 @@ export default {
components: { NavHeader },
data() {
return {
statusBarHeight: 0,
levelRows: [
{ level: "初级", points: "0", name: "筑巢会员" },
{ level: "中级", points: "1000", name: "安居会员" },
@ -112,24 +116,49 @@ export default {
],
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
},
onLoad() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
},
};
</script>
<style lang="scss" scoped>
.rules-page {
height: 100vh;
background-color: #ffffff;
background: radial-gradient(0% 0% at 0% 0%, #ffffff 0%, #e2e8f1 100%);
display: flex;
flex-direction: column;
overflow: hidden;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: radial-gradient(0% 0% at 0% 0%, #ffffff 0%, #e2e8f1 100%);
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.content-scroll {
flex: 1;
height: 0;
padding: 20rpx 24rpx 40rpx;
box-sizing: border-box;
background: #ffffff;
background: radial-gradient(0% 0% at 0% 0%, #ffffff 0%, #e2e8f1 100%);
}
.section {

View File

@ -1,11 +1,15 @@
<template>
<view class="privacy-page">
<NavHeader title="隐私政策" />
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="隐私政策" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<scroll-view class="content" scroll-y="true">
<view class="privacy-content">
<text class="privacy-text">{{ privacyText }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
@ -18,6 +22,7 @@ export default {
},
data() {
return {
statusBarHeight: 0,
privacyText: `隐私政策
本隐私政策以下简称"本政策"由广厦千万间司机公会以下简称"我们"制定旨在说明我们通过运营的微信小程序以下简称"小程序"向您以下简称"用户"提供餐饮维修保险健康等服务过程中对用户个人信息的收集使用存储传输共享披露及保护等相关事宜您在使用我们的服务时即视为您已充分阅读理解并同意本政策的全部内容包括我们对本政策的后续更新如您不同意本政策请勿使用我们的服务
@ -102,7 +107,16 @@ export default {
广厦千万间司机公会`
};
}
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
},
onLoad() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
},
};
</script>
@ -114,6 +128,22 @@ export default {
flex-direction: column;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #f5f5f5;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.content {
flex: 1;
padding: 20rpx;

View File

@ -1,14 +1,25 @@
<template>
<view class="real-name-auth-page">
<!-- 头部区域 -->
<NavHeader title="实名认证" />
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="实名认证" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<!-- 加载中 -->
<view class="loading-wrap" v-if="realNameLoading">
<text class="loading-text">加载实名信息中...</text>
</view>
<!-- 表单内容区 -->
<scroll-view class="form-content" scroll-y="true">
<scroll-view class="form-content" scroll-y="true" v-else>
<!-- 基本信息 -->
<view class="form-section">
<view class="section-title">基本信息</view>
<!-- 实名状态提示不可编辑时显示 -->
<view class="status-tip" v-if="!formEditable && realNameStatus != null">
<text class="status-tip-text">当前状态{{ realNameStatusText }}不可修改</text>
</view>
<!-- 真实姓名 -->
<view class="form-item">
<view class="input-wrapper">
@ -19,6 +30,7 @@
v-model="formData.realName"
placeholder="请输入真实姓名"
maxlength="20"
:disabled="!formEditable"
/>
</view>
</view>
@ -28,6 +40,7 @@
<view class="input-wrapper">
<text class="input-label">证件类型</text>
<picker
v-if="formEditable"
mode="selector"
:range="idTypeOptions"
range-key="label"
@ -40,6 +53,7 @@
</text>
</view>
</picker>
<text v-else class="picker-text readonly">{{ idTypeLabel }}</text>
</view>
</view>
@ -53,6 +67,7 @@
v-model="formData.idNumber"
placeholder="请输入证件号码"
maxlength="30"
:disabled="!formEditable"
/>
</view>
</view>
@ -66,7 +81,7 @@
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">身份证正面</text>
<view class="upload-box" @click="chooseImage('idFrontImg')">
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('idFrontImg')">
<image
v-if="formData.idFrontImg"
class="uploaded-image"
@ -85,7 +100,7 @@
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">身份证背面</text>
<view class="upload-box" @click="chooseImage('idBackImg')">
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('idBackImg')">
<image
v-if="formData.idBackImg"
class="uploaded-image"
@ -109,7 +124,7 @@
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">驾驶证正面</text>
<view class="upload-box" @click="chooseImage('driverLicenseFrontImg')">
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('driverLicenseFrontImg')">
<image
v-if="formData.driverLicenseFrontImg"
class="uploaded-image"
@ -128,7 +143,7 @@
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">驾驶证背面</text>
<view class="upload-box" @click="chooseImage('driverLicenseBackImg')">
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('driverLicenseBackImg')">
<image
v-if="formData.driverLicenseBackImg"
class="uploaded-image"
@ -152,7 +167,7 @@
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">行驶证正面</text>
<view class="upload-box" @click="chooseImage('vehicleLicenseFrontImg')">
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('vehicleLicenseFrontImg')">
<image
v-if="formData.vehicleLicenseFrontImg"
class="uploaded-image"
@ -171,7 +186,7 @@
<view class="form-item">
<view class="upload-wrapper">
<text class="upload-label">行驶证背面</text>
<view class="upload-box" @click="chooseImage('vehicleLicenseBackImg')">
<view class="upload-box" :class="{ disabled: !formEditable }" @click="formEditable && chooseImage('vehicleLicenseBackImg')">
<image
v-if="formData.vehicleLicenseBackImg"
class="uploaded-image"
@ -199,13 +214,14 @@
placeholder="请输入备注信息(选填)"
maxlength="200"
:auto-height="true"
:disabled="!formEditable"
></textarea>
</view>
</view>
</view>
<!-- 协议同意区域 -->
<view class="agreement-section">
<!-- 协议同意区域仅可编辑时显示 -->
<view class="agreement-section" v-if="formEditable">
<view class="agreement-checkbox" @click="toggleAgreement">
<view class="checkbox-icon" :class="{ checked: agreedToTerms }">
<text v-if="agreedToTerms" class="checkmark"></text>
@ -219,8 +235,8 @@
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-section">
<!-- 提交按钮仅可编辑时显示 -->
<view class="submit-section" v-if="formEditable">
<button
class="submit-btn"
:class="{ disabled: loading || !agreedToTerms }"
@ -231,11 +247,12 @@
</button>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { createRealNameInfo } from '@/api/profile.js'
import { createRealNameInfo, getRealNameInfo } from '@/api/profile.js'
import NavHeader from "@/components/NavHeader/NavHeader.vue";
export default {
@ -244,8 +261,11 @@ export default {
},
data() {
return {
statusBarHeight: 0,
loading: false,
agreedToTerms: false, //
realNameLoading: true, //
realNameStatus: null, // 0 / 1 2 3 退 4
agreedToTerms: false, //
idTypeIndex: 0,
idTypeOptions: [
{ label: '身份证', value: 1 },
@ -265,9 +285,79 @@ export default {
}
}
},
computed: {
headerHeight() {
return this.statusBarHeight + 44
},
// (/0)退(3)(4) (1)(2)
formEditable() {
const s = this.realNameStatus
if (s == null) return true
return s === 0 || s === 3 || s === 4
},
realNameStatusText() {
const map = { 0: '未认证', 1: '待审核', 2: '已通过', 3: '已退回', 4: '已过期' }
return map[this.realNameStatus] ?? '未知'
},
idTypeLabel() {
if (this.formData.idType == null) return '请选择证件类型'
const opt = this.idTypeOptions.find(item => item.value === this.formData.idType)
return opt ? opt.label : '请选择证件类型'
}
},
onLoad() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
this.loadRealNameInfo()
},
methods: {
async loadRealNameInfo() {
this.realNameLoading = true
try {
const userInfo = uni.getStorageSync('userInfo')
if (!userInfo || !userInfo.id) {
this.realNameStatus = 0
return
}
const res = await getRealNameInfo({ id: userInfo.id })
this.fillFormFromApi(res)
} catch (e) {
console.error('获取实名信息失败:', e)
this.realNameStatus = 0
} finally {
this.realNameLoading = false
}
},
fillFormFromApi(data) {
if (!data || typeof data !== 'object') {
this.realNameStatus = 0
return
}
const s = data.status != null ? Number(data.status) : (data.id ? 1 : 0)
this.realNameStatus = s
this.formData.realName = data.realName ?? ''
this.formData.idType = data.idType != null ? data.idType : null
this.formData.idNumber = data.idNumber ?? ''
this.formData.idFrontImg = data.idFrontImg ?? ''
this.formData.idBackImg = data.idBackImg ?? ''
this.formData.driverLicenseFrontImg = data.driverLicenseFrontImg ?? ''
this.formData.driverLicenseBackImg = data.driverLicenseBackImg ?? ''
this.formData.vehicleLicenseFrontImg = data.vehicleLicenseFrontImg ?? ''
this.formData.vehicleLicenseBackImg = data.vehicleLicenseBackImg ?? ''
this.formData.imgVerifyRemark = data.imgVerifyRemark ?? ''
const idx = this.idTypeOptions.findIndex(item => item.value === this.formData.idType)
this.idTypeIndex = idx >= 0 ? idx : 0
},
toggleAgreement() {
this.agreedToTerms = !this.agreedToTerms
},
goToUserAgreement() {
uni.navigateTo({ url: '/pages/profileSub/userAgreement' })
},
goToPrivacyPolicy() {
uni.navigateTo({ url: '/pages/profileSub/privacyPolicy' })
},
//
handleIdTypeChange(e) {
@ -430,6 +520,34 @@ export default {
background: #e2e8f1;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #e2e8f1;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.loading-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 80rpx 0;
.loading-text {
font-size: 28rpx;
color: #999999;
}
}
.form-content {
height: calc(100vh - 120rpx);
padding: 0 30rpx 40rpx;
@ -446,6 +564,17 @@ export default {
margin-bottom: 24rpx;
padding-left: 10rpx;
}
.status-tip {
margin-bottom: 20rpx;
padding: 16rpx 20rpx;
background: rgba(255, 152, 0, 0.1);
border-radius: 12rpx;
.status-tip-text {
font-size: 26rpx;
color: #e65100;
}
}
}
.form-item {
@ -487,6 +616,10 @@ export default {
&.placeholder {
color: #999999;
}
&.readonly {
color: #666666;
}
}
.picker-arrow {
@ -546,6 +679,11 @@ export default {
color: #999999;
}
}
&.disabled {
pointer-events: none;
opacity: 0.85;
}
}
}

View File

@ -1,8 +1,9 @@
<template>
<view class="service-records-page">
<!-- 头部区域 -->
<NavHeader title="服务记录" />
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="服务记录" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<!-- Tab 切换 -->
<view class="tab-bar">
<view
@ -119,6 +120,7 @@
src="/static/home/qr_code_icon.png"
mode="aspectFill"
class="qr-code-icon"
:class="{ gray: item.status === 4 }"
></image>
</view>
</view>
@ -217,6 +219,7 @@
</view>
</view>
</view>
</view>
</view>
</template>
@ -230,6 +233,7 @@ export default {
},
data() {
return {
statusBarHeight: 0,
currentTab: "pending_payment", // tab
refreshing: false,
loading: false,
@ -252,6 +256,9 @@ export default {
};
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
currentList() {
return this.recordsMap[this.currentTab] || [];
},
@ -262,7 +269,8 @@ export default {
},
},
onLoad(options) {
// tab使 tab
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
if (options && options.tab) {
this.currentTab = options.tab;
}
@ -577,6 +585,22 @@ export default {
flex-direction: column;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #e2e8f1;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* Tab 切换栏 */
.tab-bar {
display: flex;
@ -843,6 +867,11 @@ export default {
.qr-code-icon {
width: 44rpx;
height: 44rpx;
&.gray {
filter: grayscale(100%);
opacity: 0.6;
}
}
}
}

View File

@ -1,11 +1,15 @@
<template>
<view class="agreement-page">
<NavHeader title="用户服务协议" />
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="用户服务协议" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<scroll-view class="content" scroll-y="true">
<view class="agreement-content">
<text class="agreement-text">{{ agreementText }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
@ -18,6 +22,7 @@ export default {
},
data() {
return {
statusBarHeight: 0,
agreementText: `用户服务协议
鉴于甲方运营微信小程序以下简称"小程序"为乙方提供餐饮维修保险健康等专属收费服务乙方自愿使用甲方提供的服务为明确双方权利义务根据中华人民共和国民法典中华人民共和国个人信息保护法等相关法律法规甲乙双方本着平等自愿公平诚实信用的原则达成如下协议以资共同遵守
@ -98,7 +103,16 @@ export default {
10.2 甲方通过小程序公告短信发送的通知公示自发布或送达之日起生效视为已履行告知义务乙方应及时关注
10.3 本协议条款被认定为无效或不可执行的不影响其他条款的效力`
};
}
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
},
onLoad() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
},
};
</script>
@ -110,6 +124,22 @@ export default {
flex-direction: column;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #f5f5f5;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.content {
flex: 1;
padding: 20rpx;

View File

@ -1,9 +1,9 @@
<template>
<view class="webview-page">
<!-- 头部导航栏 -->
<NavHeader title="网页" />
<!-- WebView 容器 -->
<view class="header-fixed-wrapper" :style="{ height: headerHeight + 'px' }">
<NavHeader title="网页" />
</view>
<view class="main-wrap" :style="{ paddingTop: headerHeight + 'px' }">
<view class="webview-container">
<!-- #ifdef MP-WEIXIN -->
<web-view :src="webviewUrl"></web-view>
@ -15,6 +15,7 @@
</view>
<!-- #endif -->
</view>
</view>
</view>
</template>
@ -27,10 +28,18 @@ export default {
},
data() {
return {
statusBarHeight: 0,
webviewUrl: ''
}
},
computed: {
headerHeight() {
return this.statusBarHeight + 44;
},
},
onLoad(options) {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
// URL
if (options.url) {
this.webviewUrl = decodeURIComponent(options.url);
@ -56,6 +65,22 @@ export default {
overflow: hidden;
}
.header-fixed-wrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #ffffff;
}
.main-wrap {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.webview-container {
flex: 1;
height: 0; // flex: 1 使

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB