Initial commit
|
|
@ -0,0 +1,42 @@
|
|||
# 依赖
|
||||
node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# 编译输出
|
||||
unpackage/
|
||||
dist/
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 历史记录
|
||||
.history/
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# 环境变量
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<script>
|
||||
export default {
|
||||
globalData: {
|
||||
// 用于从首页跳转到服务页面时传递需要高亮的分类
|
||||
serviceCategory: null
|
||||
},
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus()
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
},
|
||||
methods: {
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
const token = uni.getStorageSync('token')
|
||||
|
||||
// 如果有token,且当前在登录页,则跳转到首页
|
||||
if (token) {
|
||||
// 延迟一下,确保页面已经初始化
|
||||
setTimeout(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const currentRoute = currentPage ? currentPage.route : ''
|
||||
|
||||
// 如果当前在登录页,跳转到首页
|
||||
if (currentRoute === 'pages/login/login') {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index',
|
||||
fail: () => {
|
||||
// 如果switchTab失败(可能不在tabBar页面),使用reLaunch
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
</style>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* 认证相关接口
|
||||
*/
|
||||
|
||||
import { request } from './index.js'
|
||||
|
||||
/**
|
||||
* 账户密码登录
|
||||
* @param {Object} data 登录数据
|
||||
* @param {String} data.mobile 手机号
|
||||
* @param {String} data.password 密码
|
||||
* @returns {Promise} 返回登录结果(包含token等)
|
||||
*/
|
||||
export function login(data) {
|
||||
return request({
|
||||
url: '/app-api/member/auth/login',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
showLoading: true,
|
||||
needAuth: false // 登录接口不需要token认证
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序一键授权手机号登录
|
||||
* @param {Object} data 登录数据
|
||||
* @param {String} data.code 微信授权code
|
||||
* @param {String} data.encryptedData 加密数据
|
||||
* @param {String} data.iv 初始向量
|
||||
* @returns {Promise} 返回登录结果(包含token等)
|
||||
*/
|
||||
export function loginByPhone(data) {
|
||||
return request({
|
||||
url: '/app-api/member/auth/login',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
showLoading: true,
|
||||
needAuth: false // 登录接口不需要token认证
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* @param {String} refreshToken 刷新令牌
|
||||
* @returns {Promise} 返回新的token信息
|
||||
*/
|
||||
export function refreshToken(refreshToken) {
|
||||
return request({
|
||||
url: '/app-api/member/auth/refresh', // 根据实际接口调整
|
||||
method: 'POST',
|
||||
data: { refreshToken },
|
||||
showLoading: false,
|
||||
needAuth: false // 刷新token接口不需要accessToken认证
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* 首页相关接口
|
||||
*/
|
||||
|
||||
import { request } from './index.js'
|
||||
|
||||
/**
|
||||
* 获取首页数据
|
||||
* @param {Object} params 查询参数
|
||||
* @param {Number} params.noticeType 通知类型
|
||||
* @returns {Promise} 返回首页数据(招募信息、公会福利、公会活动等)
|
||||
*/
|
||||
export function getHomeData(params = {}) {
|
||||
return request({
|
||||
url: '/app-api/member/labor-union-notice/page',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
showLoading: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公会福利列表
|
||||
* @param {Object} params 查询参数
|
||||
* @returns {Promise} 返回公会福利列表
|
||||
*/
|
||||
export function getGuildBenefits(params = {}) {
|
||||
return request({
|
||||
url: '/api/guild/benefits',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公会活动列表
|
||||
* @param {Object} params 查询参数
|
||||
* @param {Number} params.page 页码
|
||||
* @param {Number} params.pageSize 每页数量
|
||||
* @returns {Promise} 返回公会活动列表
|
||||
*/
|
||||
export function getGuildActivities(params = {}) {
|
||||
return request({
|
||||
url: '/api/guild/activities',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 10,
|
||||
...params
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工会详情
|
||||
* @param {Number} id 工会ID
|
||||
* @returns {Promise} 返回工会详情
|
||||
*/
|
||||
export function getGuildDetail(id) {
|
||||
return request({
|
||||
url: '/app-api/member/labor-union-notice/get',
|
||||
method: 'GET',
|
||||
data: {
|
||||
id: id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入工会
|
||||
* @param {Object} data 加入工会的数据
|
||||
* @returns {Promise} 返回加入结果
|
||||
*/
|
||||
export function joinGuild(data = {}) {
|
||||
return request({
|
||||
url: '/api/guild/join',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
showLoading: true,
|
||||
needAuth: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 参与活动
|
||||
* @param {String|Object} activityIdOrData 活动ID或包含活动ID的对象
|
||||
* @returns {Promise} 返回参与结果
|
||||
*/
|
||||
export function joinActivity(activityIdOrData) {
|
||||
// 兼容传入字符串ID或对象的情况
|
||||
const data = typeof activityIdOrData === 'string'
|
||||
? { activityId: activityIdOrData }
|
||||
: activityIdOrData
|
||||
|
||||
return request({
|
||||
url: '/api/activity/join',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
showLoading: true,
|
||||
needAuth: true
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
/**
|
||||
* 接口请求封装
|
||||
* 统一处理请求拦截、响应拦截、错误处理、token管理等
|
||||
*/
|
||||
|
||||
// 基础URL配置(注意:末尾不要加斜杠)
|
||||
const BASE_URL = 'https://siji.chenjuncn.top'
|
||||
|
||||
// 是否正在刷新token(防止并发刷新)
|
||||
let isRefreshing = false
|
||||
// 等待刷新完成的请求队列
|
||||
let refreshSubscribers = []
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* @param {String} refreshToken 刷新令牌
|
||||
* @returns {Promise} 返回刷新结果
|
||||
*/
|
||||
function refreshAccessToken(refreshToken) {
|
||||
if (isRefreshing) {
|
||||
// 如果正在刷新,返回一个Promise,等待刷新完成
|
||||
return new Promise((resolve, reject) => {
|
||||
refreshSubscribers.push({ resolve, reject })
|
||||
})
|
||||
}
|
||||
|
||||
isRefreshing = true
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 构建完整URL
|
||||
const fullUrl = `${BASE_URL}/app-api/member/auth/refresh`
|
||||
|
||||
// 构建请求头
|
||||
const requestHeader = {
|
||||
'Content-Type': 'application/json',
|
||||
'tenant-id': '1'
|
||||
}
|
||||
|
||||
// 发起刷新请求
|
||||
uni.request({
|
||||
url: fullUrl,
|
||||
method: 'POST',
|
||||
data: { refreshToken },
|
||||
header: requestHeader,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
// 处理响应数据
|
||||
const responseData = res.data
|
||||
if (responseData.code === 0 || responseData.code === 200) {
|
||||
const data = responseData.data || responseData
|
||||
|
||||
// 保存新的token
|
||||
const token = data?.accessToken || data?.token || data?.access_token
|
||||
if (token) {
|
||||
uni.setStorageSync('token', token)
|
||||
}
|
||||
|
||||
// 保存新的refreshToken(如果有)
|
||||
const newRefreshToken = data?.refreshToken
|
||||
if (newRefreshToken) {
|
||||
uni.setStorageSync('refreshToken', newRefreshToken)
|
||||
}
|
||||
|
||||
// 保存新的过期时间
|
||||
const expiresTime = data?.expiresTime
|
||||
if (expiresTime) {
|
||||
uni.setStorageSync('tokenExpiresTime', expiresTime)
|
||||
}
|
||||
|
||||
// 通知所有等待的请求
|
||||
refreshSubscribers.forEach(subscriber => subscriber.resolve())
|
||||
refreshSubscribers = []
|
||||
isRefreshing = false
|
||||
|
||||
resolve(data)
|
||||
} else {
|
||||
// 刷新失败
|
||||
const errorMsg = responseData.message || responseData.msg || '刷新token失败'
|
||||
refreshSubscribers.forEach(subscriber => subscriber.reject(new Error(errorMsg)))
|
||||
refreshSubscribers = []
|
||||
isRefreshing = false
|
||||
reject(new Error(errorMsg))
|
||||
}
|
||||
} else {
|
||||
// 刷新失败
|
||||
refreshSubscribers.forEach(subscriber => subscriber.reject(new Error('刷新token失败')))
|
||||
refreshSubscribers = []
|
||||
isRefreshing = false
|
||||
reject(new Error('刷新token失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
// 刷新失败
|
||||
refreshSubscribers.forEach(subscriber => subscriber.reject(err))
|
||||
refreshSubscribers = []
|
||||
isRefreshing = false
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一请求方法
|
||||
* @param {Object} options 请求配置
|
||||
* @param {String} options.url 请求地址(相对路径,会自动拼接BASE_URL)
|
||||
* @param {String} options.method 请求方法,默认GET
|
||||
* @param {Object} options.data 请求参数
|
||||
* @param {Object} options.header 请求头
|
||||
* @param {Boolean} options.showLoading 是否显示loading,默认false
|
||||
* @param {Boolean} options.needAuth 是否需要token认证,默认true
|
||||
* @returns {Promise} 返回处理后的响应数据
|
||||
*/
|
||||
export function request(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const {
|
||||
url,
|
||||
method = 'GET',
|
||||
data = {},
|
||||
header = {},
|
||||
showLoading = false,
|
||||
needAuth = true
|
||||
} = options
|
||||
|
||||
// 显示loading
|
||||
if (showLoading) {
|
||||
uni.showLoading({
|
||||
title: '加载中...',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
|
||||
// 构建完整URL(处理BASE_URL末尾斜杠问题)
|
||||
const fullUrl = url.startsWith('http') ? url : `${BASE_URL}${url.startsWith('/') ? url : '/' + url}`
|
||||
|
||||
// 构建请求头
|
||||
const requestHeader = {
|
||||
'Content-Type': 'application/json',
|
||||
'tenant-id': '1', // 统一添加租户ID
|
||||
...header
|
||||
}
|
||||
|
||||
// 添加token(如果需要认证)
|
||||
if (needAuth) {
|
||||
const token = uni.getStorageSync('token')
|
||||
if (token) {
|
||||
requestHeader['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
uni.request({
|
||||
url: fullUrl,
|
||||
method: method.toUpperCase(),
|
||||
data: data,
|
||||
header: requestHeader,
|
||||
success: (res) => {
|
||||
// 隐藏loading
|
||||
if (showLoading) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
// 统一处理响应
|
||||
if (res.statusCode === 200) {
|
||||
// 根据实际后端返回的数据结构处理
|
||||
// 如果后端返回格式为 { code: 200, data: {}, message: '' }
|
||||
if (res.data && typeof res.data === 'object') {
|
||||
// 如果后端有统一的code字段
|
||||
if (res.data.code !== undefined) {
|
||||
// 处理业务错误码401(账号未登录)
|
||||
if (res.data.code === 401) {
|
||||
// 显示登录弹窗
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: res.data.msg || res.data.message || '账号未登录,请前往登录',
|
||||
showCancel: false,
|
||||
confirmText: '去登录',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
// 清除本地存储的登录信息
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshToken')
|
||||
uni.removeStorageSync('tokenExpiresTime')
|
||||
uni.removeStorageSync('userId')
|
||||
uni.removeStorageSync('userInfo')
|
||||
// 跳转到登录页面
|
||||
uni.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
reject(new Error('未授权'))
|
||||
return
|
||||
}
|
||||
if (res.data.code === 200 || res.data.code === 0) {
|
||||
resolve(res.data.data || res.data)
|
||||
} else {
|
||||
// 业务错误
|
||||
const errorMsg = res.data.message || res.data.msg || '请求失败'
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
reject(new Error(errorMsg))
|
||||
}
|
||||
} else {
|
||||
// 没有code字段,直接返回data
|
||||
resolve(res.data)
|
||||
}
|
||||
} else {
|
||||
resolve(res.data)
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
// token过期或未登录,尝试使用refreshToken刷新
|
||||
const refreshToken = uni.getStorageSync('refreshToken')
|
||||
if (refreshToken) {
|
||||
// 尝试刷新token
|
||||
refreshAccessToken(refreshToken)
|
||||
.then(() => {
|
||||
// 刷新成功,重新发起原请求
|
||||
return request(options)
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(() => {
|
||||
// 刷新失败,显示登录弹窗
|
||||
const errorMsg = res.data?.msg || res.data?.message || '账号未登录,请前往登录'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: errorMsg,
|
||||
showCancel: false,
|
||||
confirmText: '去登录',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
// 清除token,跳转到登录页
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshToken')
|
||||
uni.removeStorageSync('tokenExpiresTime')
|
||||
uni.removeStorageSync('userId')
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
reject(new Error('未授权'))
|
||||
})
|
||||
} else {
|
||||
// 没有refreshToken,显示登录弹窗
|
||||
const errorMsg = res.data?.msg || res.data?.message || '账号未登录,请前往登录'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: errorMsg,
|
||||
showCancel: false,
|
||||
confirmText: '去登录',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
// 清除token,跳转到登录页
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshToken')
|
||||
uni.removeStorageSync('tokenExpiresTime')
|
||||
uni.removeStorageSync('userId')
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
reject(new Error('未授权'))
|
||||
}
|
||||
} else if (res.statusCode >= 500) {
|
||||
// 服务器错误
|
||||
uni.showToast({
|
||||
title: '服务器错误,请稍后重试',
|
||||
icon: 'none'
|
||||
})
|
||||
reject(new Error('服务器错误'))
|
||||
} else {
|
||||
// 其他错误
|
||||
const errorMsg = res.data?.message || res.data?.msg || `请求失败(${res.statusCode})`
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none'
|
||||
})
|
||||
reject(new Error(errorMsg))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
// 隐藏loading
|
||||
if (showLoading) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
// 网络错误处理
|
||||
let errorMsg = '网络请求失败'
|
||||
if (err.errMsg) {
|
||||
if (err.errMsg.includes('timeout')) {
|
||||
errorMsg = '请求超时,请检查网络'
|
||||
} else if (err.errMsg.includes('fail')) {
|
||||
errorMsg = '网络连接失败,请检查网络设置'
|
||||
}
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* 个人中心相关接口
|
||||
*/
|
||||
|
||||
import { request } from './index.js'
|
||||
|
||||
/**
|
||||
* 获取个人信息
|
||||
* @returns {Promise} 返回用户信息
|
||||
*/
|
||||
export function getUserInfo() {
|
||||
return request({
|
||||
url: '/app-api/member/user/get',
|
||||
method: 'GET',
|
||||
showLoading: false,
|
||||
needAuth: true // 需要token认证
|
||||
})
|
||||
}
|
||||
|
||||
// 创建实名信息
|
||||
export function createRealNameInfo(data) {
|
||||
return request({
|
||||
url: '/app-api/member/user-real-name/addAndUpdate',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
showLoading: false,
|
||||
needAuth: true // 需要token认证
|
||||
})
|
||||
}
|
||||
|
||||
// 我的收藏
|
||||
export function getMyCollect(data) {
|
||||
return request({
|
||||
url: '/app-api/member/labor-union-collect/page',
|
||||
method: 'GET',
|
||||
data: data,
|
||||
showLoading: false,
|
||||
needAuth: true // 需要token认证
|
||||
})
|
||||
}
|
||||
|
||||
// 创建会员建议
|
||||
export function createLaborUnionSuggest(data) {
|
||||
return request({
|
||||
url: '/app-api/member/labor-union-suggest/create',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
showLoading: false,
|
||||
needAuth: true // 需要token认证
|
||||
})
|
||||
}
|
||||
|
||||
// 发布消息
|
||||
export function createLaborUnionMessage(data) {
|
||||
return request({
|
||||
url: '/app-api/member/labor-union-message/create',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
showLoading: false,
|
||||
needAuth: true // 需要token认证
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { request } from './index.js'
|
||||
|
||||
// 司机工会服务分类 -- 字典
|
||||
export function getDictDataByType(params = {}) {
|
||||
return request({
|
||||
url: '/app-api/system/dict-data/type',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
|
||||
// 获得工会商店分页
|
||||
export function getGuildStorePage(params = {}) {
|
||||
return request({
|
||||
url: '/app-api/member/labor-union-shop/page',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 获得工会商店详情
|
||||
export function getGuildStoreDetail(id) {
|
||||
return request({
|
||||
url: `/app-api/member/labor-union-shop/get`,
|
||||
method: 'GET',
|
||||
data: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 获得工会优惠券
|
||||
export function getGuildCoupon(params = {}) {
|
||||
return request({
|
||||
url: '/app-api/member/labor-union-coupon/page',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<view class="nav-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="header-content">
|
||||
<view class="back-btn" v-if="showBack" @click="handleBack">
|
||||
<image
|
||||
class="back-icon"
|
||||
:src="backIcon"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<text class="header-title">{{ title }}</text>
|
||||
<view class="header-placeholder" v-if="showPlaceholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NavHeader',
|
||||
props: {
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示返回按钮
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 返回按钮图标
|
||||
backIcon: {
|
||||
type: String,
|
||||
default: '/static/service/goBack_icon.png'
|
||||
},
|
||||
// 是否显示右侧占位(用于居中标题)
|
||||
showPlaceholder: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getSystemInfo()
|
||||
},
|
||||
methods: {
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || 0
|
||||
},
|
||||
// 返回
|
||||
handleBack() {
|
||||
this.$emit('back')
|
||||
uni.navigateBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-header {
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 30rpx;
|
||||
position: relative;
|
||||
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
left: 22rpx;
|
||||
width: 17rpx;
|
||||
height: 30rpx;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
|
||||
.back-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 36rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
|
||||
.header-placeholder {
|
||||
width: 60rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import App from './App'
|
||||
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
App.mpType = 'app'
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"name" : "demo",
|
||||
"appid" : "__UNI__B358CDA",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true,
|
||||
"requiredPrivateInfos" : [
|
||||
"getLocation"
|
||||
]
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "首页",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/service/service",
|
||||
"style": {
|
||||
"navigationBarTitleText": "服务",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人中心",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/realNameAuth",
|
||||
"style": {
|
||||
"navigationBarTitleText": "实名认证",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/serviceRecords",
|
||||
"style": {
|
||||
"navigationBarTitleText": "服务记录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "pages/detail",
|
||||
"pages": [
|
||||
{
|
||||
"path": "serviceDetail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "店铺详情",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "activitiesDetail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工会详情",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "mapDetail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "地图",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/activities",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工会活动",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "myCollect",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的收藏",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "complaints",
|
||||
"style": {
|
||||
"navigationBarTitleText": "投诉建议",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "postMessage",
|
||||
"style": {
|
||||
"navigationBarTitleText": "发布消息",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#7A7E83",
|
||||
"selectedColor": "#004294",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#ffffff",
|
||||
"iconWidth": "41rpx",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "static/tabbar/home.png",
|
||||
"selectedIconPath": "static/tabbar/home-active.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/service/service",
|
||||
"iconPath": "static/tabbar/service.png",
|
||||
"selectedIconPath": "static/tabbar/service-active.png",
|
||||
"text": "服务"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"iconPath": "static/tabbar/profile.png",
|
||||
"selectedIconPath": "static/tabbar/profile-active.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
<template>
|
||||
<view class="complaints-page">
|
||||
<!-- 头部区域 -->
|
||||
<NavHeader title="投诉建议" />
|
||||
|
||||
<!-- 表单内容区 -->
|
||||
<scroll-view class="form-content" scroll-y="true">
|
||||
<!-- 投诉建议表单 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">填写投诉建议</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">标题 <text class="required">*</text></text>
|
||||
<input
|
||||
class="input-field"
|
||||
type="text"
|
||||
v-model="formData.title"
|
||||
placeholder="请输入标题"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容 -->
|
||||
<view class="form-item">
|
||||
<view class="textarea-wrapper">
|
||||
<text class="textarea-label">内容 <text class="optional">(选填)</text></text>
|
||||
<textarea
|
||||
class="textarea-field"
|
||||
v-model="formData.content"
|
||||
placeholder="请详细描述您的投诉或建议..."
|
||||
maxlength="1000"
|
||||
:auto-height="true"
|
||||
></textarea>
|
||||
<view class="char-count">
|
||||
<text>{{ formData.content.length }}/1000</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<button
|
||||
class="submit-btn"
|
||||
:class="{ disabled: loading || !canSubmit }"
|
||||
:disabled="loading || !canSubmit"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? '提交中...' : '提交' }}
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createLaborUnionSuggest } from '@/api/profile.js'
|
||||
import NavHeader from "@/components/NavHeader/NavHeader.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
formData: {
|
||||
title: '',
|
||||
content: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 是否可以提交
|
||||
canSubmit() {
|
||||
return this.formData.title && this.formData.title.trim().length >= 1
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
},
|
||||
methods: {
|
||||
// 提交表单
|
||||
async handleSubmit() {
|
||||
// 验证标题
|
||||
if (!this.formData.title || this.formData.title.trim().length < 1) {
|
||||
uni.showToast({
|
||||
title: '请输入标题',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true
|
||||
|
||||
// 构建提交数据
|
||||
const submitData = {
|
||||
title: this.formData.title.trim(),
|
||||
content: this.formData.content ? this.formData.content.trim() : ''
|
||||
}
|
||||
|
||||
// 调用接口
|
||||
const res = await createLaborUnionSuggest(submitData)
|
||||
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
console.error('提交投诉建议失败:', error)
|
||||
uni.showToast({
|
||||
title: error.message || '提交失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.complaints-page {
|
||||
min-height: 100vh;
|
||||
background: #e2e8f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 0 30rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-top: 30rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 24rpx;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.input-label {
|
||||
width: 160rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
|
||||
.required {
|
||||
color: #d51c3c;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.optional {
|
||||
color: #999999;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.textarea-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.optional {
|
||||
color: #999999;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-field {
|
||||
width: 100%;
|
||||
min-height: 300rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
line-height: 1.6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
margin-top: 40rpx;
|
||||
padding-bottom: 40rpx;
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #004294 0%, #0066cc 100%);
|
||||
border-radius: 44rpx;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #cccccc;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,332 @@
|
|||
<template>
|
||||
<view class="activities-list-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<NavHeader title="工会活动" :show-placeholder="true" />
|
||||
|
||||
<!-- 活动列表 -->
|
||||
<scroll-view
|
||||
class="activities-scroll"
|
||||
scroll-y="true"
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="handleRefresh"
|
||||
@scrolltolower="handleLoadMore"
|
||||
:lower-threshold="100"
|
||||
>
|
||||
<view class="activities-list">
|
||||
<!-- 空数据提示 -->
|
||||
<view class="empty-state" v-if="!loading && activitiesList.length === 0">
|
||||
<image
|
||||
class="empty-icon"
|
||||
src="/static/home/entry_icon.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="empty-text">暂无活动数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 活动列表项 -->
|
||||
<view
|
||||
class="activity-item"
|
||||
v-for="(item, index) in activitiesList"
|
||||
:key="index"
|
||||
@click="handleActivityClick(item)"
|
||||
>
|
||||
<view class="activity-image-wrapper">
|
||||
<image
|
||||
class="activity-image"
|
||||
:src="item.coverUrl"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<view class="activity-content">
|
||||
<view class="activity-title">{{ item.title }}</view>
|
||||
<view class="activity-desc">{{ item.summary }}</view>
|
||||
<view class="activity-time"
|
||||
>结束时间:{{ formatTime(item.endTime) }}</view
|
||||
>
|
||||
<view
|
||||
class="join-activity-btn"
|
||||
@click.stop="handleJoinActivity(item)"
|
||||
>
|
||||
<text>立即参与</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view class="load-more" v-if="activitiesList.length > 0">
|
||||
<text v-if="loadingMore" class="load-more-text">加载中...</text>
|
||||
<text v-else-if="!hasMore" class="load-more-text">没有更多数据了</text>
|
||||
<text v-else class="load-more-text">上拉加载更多</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getHomeData } from "@/api/home.js";
|
||||
import { formatTime } from "@/utils/date.js";
|
||||
import NavHeader from "@/components/NavHeader/NavHeader.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activitiesList: [],
|
||||
// 分页相关
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
refreshing: false,
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.loadActivities();
|
||||
},
|
||||
methods: {
|
||||
// 加载活动列表
|
||||
async loadActivities(isLoadMore = false) {
|
||||
// 如果正在加载或没有更多数据,则不加载
|
||||
if (this.loading || this.loadingMore || (!isLoadMore && !this.hasMore)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isLoadMore) {
|
||||
this.loadingMore = true;
|
||||
} else {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
const res = await getHomeData({
|
||||
page: this.page,
|
||||
pageSize: this.pageSize,
|
||||
noticeType: 2, // 活动类型
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const newList = res.list || [];
|
||||
this.total = res.total || 0;
|
||||
|
||||
if (isLoadMore) {
|
||||
// 加载更多,追加数据
|
||||
this.activitiesList = [...this.activitiesList, ...newList];
|
||||
} else {
|
||||
// 首次加载或刷新,替换数据
|
||||
this.activitiesList = newList;
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
this.hasMore = this.activitiesList.length < this.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载活动列表失败:", error);
|
||||
uni.showToast({
|
||||
title: "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.loadingMore = false;
|
||||
this.refreshing = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
handleRefresh() {
|
||||
this.refreshing = true;
|
||||
this.page = 1;
|
||||
this.hasMore = true;
|
||||
this.loadActivities(false);
|
||||
},
|
||||
|
||||
// 上拉加载更多
|
||||
handleLoadMore() {
|
||||
if (this.hasMore && !this.loadingMore && !this.loading) {
|
||||
this.page += 1;
|
||||
this.loadActivities(true);
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化时间戳
|
||||
formatTime,
|
||||
|
||||
// 活动项点击
|
||||
handleActivityClick(item) {
|
||||
// 后续可以跳转到活动详情页
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/activities/detail?id=${item.id}`
|
||||
// })
|
||||
},
|
||||
|
||||
// 参与活动
|
||||
async handleJoinActivity(item) {
|
||||
// 跳转到活动详情页
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/activitiesDetail?id=${item.id}`
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.activities-list-page {
|
||||
height: 100vh;
|
||||
background-color: #e2e8f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 活动列表 */
|
||||
.activities-scroll {
|
||||
flex: 1;
|
||||
height: 0; // 配合 flex: 1 使用,让 scroll-view 可以滚动
|
||||
}
|
||||
|
||||
.activities-list {
|
||||
padding: 20rpx;
|
||||
background-color: #e2e8f1;
|
||||
|
||||
/* 空数据提示 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
min-height: 500rpx;
|
||||
|
||||
.empty-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 40rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载更多提示 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
min-height: 80rpx;
|
||||
|
||||
.load-more-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
padding: 25rpx 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.activity-image-wrapper {
|
||||
width: 186rpx;
|
||||
height: 200rpx;
|
||||
flex-shrink: 0;
|
||||
border: 1rpx solid #e2e8f1;
|
||||
border-radius: 10rpx;
|
||||
margin-right: 20rpx;
|
||||
|
||||
.activity-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
||||
.activity-title {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
color: #1a1819;
|
||||
margin-bottom: 12rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
width: 260rpx;
|
||||
height: 33rpx;
|
||||
padding: 0 10rpx;
|
||||
background: rgba(0, 66, 148, 0.1);
|
||||
color: #004294;
|
||||
font-size: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.join-activity-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #004294;
|
||||
border-radius: 25rpx;
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
cursor: pointer;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
<template>
|
||||
<view class="my-collect-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<NavHeader title="我的收藏" />
|
||||
|
||||
<!-- 收藏列表 -->
|
||||
<scroll-view
|
||||
class="collect-scroll"
|
||||
scroll-y="true"
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="handleRefresh"
|
||||
@scrolltolower="handleLoadMore"
|
||||
:lower-threshold="100"
|
||||
>
|
||||
<view class="collect-list">
|
||||
<!-- 空数据提示 -->
|
||||
<view class="empty-state" v-if="!loading && collectList.length === 0">
|
||||
<image
|
||||
class="empty-icon"
|
||||
src="/static/home/entry_icon.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="empty-text">暂无收藏数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 收藏列表项 -->
|
||||
<view
|
||||
class="collect-item"
|
||||
v-for="(item, index) in collectList"
|
||||
:key="index"
|
||||
@click="handleCollectClick(item)"
|
||||
>
|
||||
<view class="collect-image-wrapper" v-if="item.coverUrl">
|
||||
<image
|
||||
class="collect-image"
|
||||
:src="item.coverUrl"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<view class="collect-content">
|
||||
<view class="collect-title">{{ item.title || item.collectName || '未命名' }}</view>
|
||||
<view class="collect-desc" v-if="item.info">{{ item.info }}</view>
|
||||
<view class="collect-time">{{ formatTime(item.createTime) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view class="load-more" v-if="collectList.length > 0">
|
||||
<text v-if="loadingMore" class="load-more-text">加载中...</text>
|
||||
<text v-else-if="!hasMore" class="load-more-text">没有更多数据了</text>
|
||||
<text v-else class="load-more-text">上拉加载更多</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getMyCollect } from "@/api/profile.js";
|
||||
import NavHeader from "@/components/NavHeader/NavHeader.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collectList: [],
|
||||
// 分页相关
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
refreshing: false,
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.loadCollectList();
|
||||
},
|
||||
methods: {
|
||||
// 加载收藏列表
|
||||
async loadCollectList(isLoadMore = false) {
|
||||
// 如果正在加载或没有更多数据,则不加载
|
||||
if (this.loading || this.loadingMore || (!isLoadMore && !this.hasMore)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isLoadMore) {
|
||||
this.loadingMore = true;
|
||||
} else {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
const res = await getMyCollect({
|
||||
pageNo: this.pageNo,
|
||||
pageSize: this.pageSize,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const newList = res.list || [];
|
||||
this.total = res.total || 0;
|
||||
|
||||
if (isLoadMore) {
|
||||
// 加载更多,追加数据
|
||||
this.collectList = [...this.collectList, ...newList];
|
||||
} else {
|
||||
// 首次加载或刷新,替换数据
|
||||
this.collectList = newList;
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
this.hasMore = this.collectList.length < this.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载收藏列表失败:", error);
|
||||
uni.showToast({
|
||||
title: "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.loadingMore = false;
|
||||
this.refreshing = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
handleRefresh() {
|
||||
this.refreshing = true;
|
||||
this.pageNo = 1;
|
||||
this.hasMore = true;
|
||||
this.loadCollectList(false);
|
||||
},
|
||||
|
||||
// 上拉加载更多
|
||||
handleLoadMore() {
|
||||
if (this.hasMore && !this.loadingMore && !this.loading) {
|
||||
this.pageNo += 1;
|
||||
this.loadCollectList(true);
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timeStr) {
|
||||
if (!timeStr) return "";
|
||||
const date = new Date(timeStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
},
|
||||
|
||||
// 收藏项点击
|
||||
handleCollectClick(item) {
|
||||
// 根据收藏类型跳转到对应的详情页
|
||||
// collectType: 收藏的类型
|
||||
// collectId: 收藏的id
|
||||
if (item.collectType && item.collectId) {
|
||||
// 这里可以根据 collectType 判断跳转到不同的详情页
|
||||
// 例如:1-活动详情,2-服务详情等
|
||||
if (item.collectType === 2) {
|
||||
// 假设类型2是活动
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/activitiesDetail?id=${item.collectId}`,
|
||||
});
|
||||
} else {
|
||||
// 其他类型可以在这里扩展
|
||||
uni.showToast({
|
||||
title: "暂不支持查看详情",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-collect-page {
|
||||
height: 100vh;
|
||||
background-color: #e2e8f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 收藏列表 */
|
||||
.collect-scroll {
|
||||
flex: 1;
|
||||
height: 0; // 配合 flex: 1 使用,让 scroll-view 可以滚动
|
||||
}
|
||||
|
||||
.collect-list {
|
||||
padding: 20rpx;
|
||||
background-color: #e2e8f1;
|
||||
|
||||
/* 空数据提示 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
min-height: 500rpx;
|
||||
|
||||
.empty-icon {
|
||||
width: 356rpx;
|
||||
height: 266rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载更多提示 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
min-height: 80rpx;
|
||||
|
||||
.load-more-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.collect-item {
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
padding: 25rpx 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.collect-image-wrapper {
|
||||
width: 186rpx;
|
||||
height: 200rpx;
|
||||
flex-shrink: 0;
|
||||
border: 1rpx solid #e2e8f1;
|
||||
border-radius: 10rpx;
|
||||
margin-right: 20rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.collect-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.collect-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.collect-title {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
color: #1a1819;
|
||||
margin-bottom: 12rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.collect-desc {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.collect-time {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 400;
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,617 @@
|
|||
<template>
|
||||
<view class="post-message-page">
|
||||
<!-- 头部区域 -->
|
||||
<NavHeader title="发布消息" />
|
||||
|
||||
<!-- 表单内容区 -->
|
||||
<scroll-view class="form-content" scroll-y="true">
|
||||
<!-- 基本信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">基本信息</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">标题</text>
|
||||
<input
|
||||
class="input-field"
|
||||
type="text"
|
||||
v-model="formData.title"
|
||||
placeholder="请输入消息标题"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 摘要 -->
|
||||
<view class="form-item">
|
||||
<view class="textarea-wrapper">
|
||||
<text class="textarea-label">摘要 <text class="optional">(选填)</text></text>
|
||||
<textarea
|
||||
class="textarea-field"
|
||||
v-model="formData.summary"
|
||||
placeholder="请输入消息摘要..."
|
||||
maxlength="200"
|
||||
:auto-height="true"
|
||||
></textarea>
|
||||
<view class="char-count">
|
||||
<text>{{ formData.summary.length }}/200</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容 -->
|
||||
<view class="form-item">
|
||||
<view class="textarea-wrapper">
|
||||
<text class="textarea-label">内容 <text class="optional">(选填)</text></text>
|
||||
<textarea
|
||||
class="textarea-field"
|
||||
v-model="formData.content"
|
||||
placeholder="请输入消息详细内容..."
|
||||
maxlength="2000"
|
||||
:auto-height="true"
|
||||
></textarea>
|
||||
<view class="char-count">
|
||||
<text>{{ formData.content.length }}/2000</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 封面图片 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">封面图片</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="upload-wrapper">
|
||||
<text class="upload-label">封面图片 <text class="optional">(选填)</text></text>
|
||||
<view class="upload-box" @click="chooseCoverImage">
|
||||
<image
|
||||
v-if="formData.coverUrl"
|
||||
class="uploaded-image"
|
||||
:src="formData.coverUrl"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">点击上传封面</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 其他设置 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">其他设置</view>
|
||||
|
||||
<!-- 跳转链接 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">跳转链接 <text class="optional">(选填)</text></text>
|
||||
<input
|
||||
class="input-field"
|
||||
type="text"
|
||||
v-model="formData.jumpUrl"
|
||||
placeholder="请输入跳转链接,如:https://www.example.com"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息类型 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">消息类型 <text class="optional">(选填)</text></text>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="messageTypeOptions"
|
||||
range-key="label"
|
||||
:value="messageTypeIndex"
|
||||
@change="handleMessageTypeChange"
|
||||
>
|
||||
<view class="picker-view">
|
||||
<text :class="['picker-text', messageTypeIndex === -1 ? 'placeholder' : '']">
|
||||
{{ messageTypeIndex !== -1 ? messageTypeOptions[messageTypeIndex].label : '请选择消息类型' }}
|
||||
</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 等级 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">等级 <text class="optional">(选填)</text></text>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="gradeOptions"
|
||||
range-key="label"
|
||||
:value="gradeIndex"
|
||||
@change="handleGradeChange"
|
||||
>
|
||||
<view class="picker-view">
|
||||
<text :class="['picker-text', gradeIndex === -1 ? 'placeholder' : '']">
|
||||
{{ gradeIndex !== -1 ? gradeOptions[gradeIndex].label : '请选择等级' }}
|
||||
</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 开始时间 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">开始时间 <text class="optional">(选填)</text></text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="startTimeDisplay"
|
||||
@change="handleStartTimeChange"
|
||||
>
|
||||
<view class="picker-view">
|
||||
<text :class="['picker-text', !startTimeDisplay ? 'placeholder' : '']">
|
||||
{{ startTimeDisplay || '请选择开始时间' }}
|
||||
</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 结束时间 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">结束时间 <text class="optional">(选填)</text></text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="endTimeDisplay"
|
||||
@change="handleEndTimeChange"
|
||||
>
|
||||
<view class="picker-view">
|
||||
<text :class="['picker-text', !endTimeDisplay ? 'placeholder' : '']">
|
||||
{{ endTimeDisplay || '请选择结束时间' }}
|
||||
</text>
|
||||
<text class="picker-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<button
|
||||
class="submit-btn"
|
||||
:class="{ disabled: loading }"
|
||||
:disabled="loading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? '发布中...' : '发布' }}
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createLaborUnionMessage } from '@/api/profile.js'
|
||||
import NavHeader from "@/components/NavHeader/NavHeader.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
messageTypeIndex: -1,
|
||||
messageTypeOptions: [
|
||||
{ label: '类型1', value: 1 },
|
||||
{ label: '类型2', value: 2 },
|
||||
{ label: '类型3', value: 3 }
|
||||
],
|
||||
gradeIndex: -1,
|
||||
gradeOptions: [
|
||||
{ label: '普通', value: 0 },
|
||||
{ label: '重要', value: 1 }
|
||||
],
|
||||
formData: {
|
||||
title: '',
|
||||
summary: '',
|
||||
content: '',
|
||||
coverUrl: '',
|
||||
jumpUrl: '',
|
||||
messageType: null,
|
||||
grade: null,
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 开始时间显示(从 ISO 格式提取日期部分)
|
||||
startTimeDisplay() {
|
||||
if (!this.formData.startTime) return ''
|
||||
return this.formData.startTime.split('T')[0]
|
||||
},
|
||||
// 结束时间显示(从 ISO 格式提取日期部分)
|
||||
endTimeDisplay() {
|
||||
if (!this.formData.endTime) return ''
|
||||
return this.formData.endTime.split('T')[0]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
},
|
||||
methods: {
|
||||
// 选择封面图片
|
||||
chooseCoverImage() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const tempFilePath = res.tempFilePaths[0]
|
||||
// 先显示本地预览
|
||||
this.formData.coverUrl = tempFilePath
|
||||
// 上传图片到服务器
|
||||
this.uploadImage(tempFilePath)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择图片失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择图片失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 上传图片
|
||||
uploadImage(filePath) {
|
||||
uni.showLoading({
|
||||
title: '上传中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 获取token
|
||||
const token = uni.getStorageSync('token')
|
||||
const BASE_URL = 'https://siji.chenjuncn.top'
|
||||
|
||||
uni.uploadFile({
|
||||
url: `${BASE_URL}/app-api/infra/file/upload`,
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'tenant-id': '1'
|
||||
},
|
||||
success: (res) => {
|
||||
uni.hideLoading()
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 200 || data.code === 0) {
|
||||
// 上传成功,保存图片URL
|
||||
const imageUrl = data.data?.url || data.data || data.url
|
||||
if (imageUrl) {
|
||||
this.formData.coverUrl = imageUrl
|
||||
uni.showToast({
|
||||
title: '上传成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
} else {
|
||||
throw new Error('上传成功但未返回图片地址')
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || data.msg || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析上传结果失败:', error)
|
||||
uni.showToast({
|
||||
title: '上传失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
// 上传失败,清除预览
|
||||
this.formData.coverUrl = ''
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('上传图片失败:', err)
|
||||
uni.showToast({
|
||||
title: '上传失败,请检查网络',
|
||||
icon: 'none'
|
||||
})
|
||||
// 上传失败,清除预览
|
||||
this.formData.coverUrl = ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 消息类型选择
|
||||
handleMessageTypeChange(e) {
|
||||
this.messageTypeIndex = e.detail.value
|
||||
this.formData.messageType = this.messageTypeOptions[e.detail.value].value
|
||||
},
|
||||
|
||||
// 等级选择
|
||||
handleGradeChange(e) {
|
||||
this.gradeIndex = e.detail.value
|
||||
this.formData.grade = this.gradeOptions[e.detail.value].value
|
||||
},
|
||||
|
||||
// 开始时间选择
|
||||
handleStartTimeChange(e) {
|
||||
// 将日期转换为 ISO 格式 (date-time)
|
||||
const date = e.detail.value
|
||||
this.formData.startTime = date ? `${date}T00:00:00` : ''
|
||||
},
|
||||
|
||||
// 结束时间选择
|
||||
handleEndTimeChange(e) {
|
||||
// 将日期转换为 ISO 格式 (date-time)
|
||||
const date = e.detail.value
|
||||
this.formData.endTime = date ? `${date}T23:59:59` : ''
|
||||
},
|
||||
|
||||
// 提交表单
|
||||
async handleSubmit() {
|
||||
try {
|
||||
this.loading = true
|
||||
|
||||
// 构建提交数据
|
||||
const submitData = {
|
||||
title: this.formData.title ? this.formData.title.trim() : '',
|
||||
summary: this.formData.summary ? this.formData.summary.trim() : '',
|
||||
content: this.formData.content ? this.formData.content.trim() : '',
|
||||
coverUrl: this.formData.coverUrl || '',
|
||||
jumpUrl: this.formData.jumpUrl ? this.formData.jumpUrl.trim() : '',
|
||||
messageType: this.formData.messageType,
|
||||
sourceType: 1, // 会员发布
|
||||
grade: this.formData.grade,
|
||||
startTime: this.formData.startTime || null,
|
||||
endTime: this.formData.endTime || null
|
||||
}
|
||||
|
||||
// 移除空值
|
||||
Object.keys(submitData).forEach(key => {
|
||||
if (submitData[key] === '' || submitData[key] === null || submitData[key] === undefined) {
|
||||
delete submitData[key]
|
||||
}
|
||||
})
|
||||
|
||||
// 调用接口
|
||||
const res = await createLaborUnionMessage(submitData)
|
||||
|
||||
uni.showToast({
|
||||
title: '发布成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
console.error('发布消息失败:', error)
|
||||
uni.showToast({
|
||||
title: error.message || '发布失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.post-message-page {
|
||||
min-height: 100vh;
|
||||
background: #e2e8f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 0 30rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-top: 30rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 24rpx;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.input-label {
|
||||
width: 160rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
|
||||
.optional {
|
||||
color: #999999;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
|
||||
&.placeholder {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 40rpx;
|
||||
color: #cccccc;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.textarea-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.optional {
|
||||
color: #999999;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-field {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
line-height: 1.6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-wrapper {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.upload-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.optional {
|
||||
color: #999999;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
border: 2rpx dashed #d0d0d0;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fafafa;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.uploaded-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
|
||||
.upload-icon {
|
||||
font-size: 60rpx;
|
||||
color: #999999;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
margin-top: 40rpx;
|
||||
padding-bottom: 40rpx;
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #004294 0%, #0066cc 100%);
|
||||
border-radius: 44rpx;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #cccccc;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
<template>
|
||||
<view class="activities-detail-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<NavHeader title="工会详情" />
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<scroll-view class="content-scroll" scroll-y="true">
|
||||
<!-- 加载中 -->
|
||||
<view class="loading-state" v-if="loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 富文本内容 -->
|
||||
<view class="content-wrapper" v-else-if="activityDetail && activityDetail.content">
|
||||
<rich-text :nodes="activityDetail.content"></rich-text>
|
||||
</view>
|
||||
|
||||
<!-- 空数据提示 -->
|
||||
<view class="empty-state" v-else>
|
||||
<image
|
||||
class="empty-icon"
|
||||
src="/static/home/entry_icon.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="empty-text">暂无内容</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getGuildDetail } from "@/api/home.js";
|
||||
import NavHeader from "@/components/NavHeader/NavHeader.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activityId: null,
|
||||
activityDetail: null,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.id) {
|
||||
this.activityId = options.id;
|
||||
this.loadActivityDetail();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "参数错误",
|
||||
icon: "none",
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载活动详情
|
||||
async loadActivityDetail() {
|
||||
if (!this.activityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
const res = await getGuildDetail(this.activityId);
|
||||
if (res) {
|
||||
this.activityDetail = res;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载活动详情失败:", error);
|
||||
uni.showToast({
|
||||
title: "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.activities-detail-page {
|
||||
height: 100vh;
|
||||
background-color: #e2e8f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
height: 0; // 配合 flex: 1 使用,让 scroll-view 可以滚动
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 30rpx 20rpx;
|
||||
background-color: #ffffff;
|
||||
margin: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
min-height: 200rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
// 富文本内容样式
|
||||
:deep(rich-text) {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
color: #333333;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 富文本内容中的元素样式
|
||||
:deep(img) {
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
display: block;
|
||||
margin: 20rpx auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 20rpx 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
:deep(h3),
|
||||
:deep(h4),
|
||||
:deep(h5),
|
||||
:deep(h6) {
|
||||
margin: 30rpx 0 20rpx 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
margin: 20rpx 0;
|
||||
padding-left: 40rpx;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
margin: 10rpx 0;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: #004294;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 200rpx 0;
|
||||
min-height: 500rpx;
|
||||
|
||||
.loading-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 空数据提示 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 200rpx 0;
|
||||
min-height: 500rpx;
|
||||
|
||||
.empty-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 40rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,492 @@
|
|||
<template>
|
||||
<view class="map-detail-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<NavHeader title="店铺位置" />
|
||||
|
||||
<!-- 地图区域 -->
|
||||
<view class="map-container" v-if="mapInitialized">
|
||||
<map
|
||||
id="storeMap"
|
||||
class="map"
|
||||
:latitude="mapCenter.latitude"
|
||||
:longitude="mapCenter.longitude"
|
||||
:markers="markers"
|
||||
:scale="16"
|
||||
:show-location="false"
|
||||
:enable-zoom="false"
|
||||
:enable-scroll="false"
|
||||
:enable-rotate="false"
|
||||
:enable-poi="true"
|
||||
></map>
|
||||
</view>
|
||||
<view class="map-container" v-else>
|
||||
<view class="map-placeholder">
|
||||
<text>加载地图中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 店铺信息卡片(悬浮在下方) -->
|
||||
<view class="store-card" v-if="storeInfo.id">
|
||||
<view class="store-card-content">
|
||||
<!-- 左侧店铺图片 -->
|
||||
<view class="store-image-wrapper">
|
||||
<image
|
||||
class="store-image"
|
||||
:src="storeInfo.coverUrl || '/static/service/store-default.png'"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<!-- 右侧店铺信息 -->
|
||||
<view class="store-info">
|
||||
<view class="store-title-row">
|
||||
<text class="store-name">{{ storeInfo.name }}</text>
|
||||
<view class="distance-info">
|
||||
<image
|
||||
class="location-icon"
|
||||
src="/static/service/location-icon.png"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<text class="distance-text">{{ storeInfo.distance || "0" }}km</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="store-detail">
|
||||
<text class="detail-label">门店地址:</text>
|
||||
<text class="detail-value">{{
|
||||
storeInfo.address || "暂无地址"
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<view class="store-detail">
|
||||
<text class="detail-label">联系电话:</text>
|
||||
<text class="detail-value">{{
|
||||
storeInfo.phone || "暂无电话"
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<view class="store-tags-row">
|
||||
<view class="store-tags">
|
||||
<view class="tag-item tag-pink">会员特惠</view>
|
||||
<view class="tag-item tag-orange" v-if="storeInfo.typeName">{{
|
||||
storeInfo.typeName
|
||||
}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <view class="enter-store-btn" @click="handleEnterStore"> 进店 </view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中提示 -->
|
||||
<view class="loading-mask" v-if="loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getGuildStoreDetail } from "@/api/service";
|
||||
import NavHeader from "@/components/NavHeader/NavHeader.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shopId: null,
|
||||
storeInfo: {},
|
||||
loading: false,
|
||||
// 地图中心点(初始值,加载数据后会更新)
|
||||
mapCenter: {
|
||||
latitude: 39.908823,
|
||||
longitude: 116.39747,
|
||||
},
|
||||
// 地图标记点
|
||||
markers: [],
|
||||
// 用户当前位置
|
||||
userLocation: null,
|
||||
// 地图是否已初始化(避免重复移动)
|
||||
mapInitialized: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// // 计算距离文本
|
||||
// distanceText() {
|
||||
// if (
|
||||
// !this.userLocation ||
|
||||
// !this.storeInfo.latitude ||
|
||||
// !this.storeInfo.longitude
|
||||
// ) {
|
||||
// return "计算中...";
|
||||
// }
|
||||
// const distance = this.calculateDistance(
|
||||
// this.userLocation.latitude,
|
||||
// this.userLocation.longitude,
|
||||
// this.storeInfo.latitude,
|
||||
// this.storeInfo.longitude
|
||||
// );
|
||||
// return distance < 1
|
||||
// ? `${(distance * 1000).toFixed(0)}m`
|
||||
// : `${distance.toFixed(1)}km`;
|
||||
// },
|
||||
},
|
||||
onLoad(options) {
|
||||
this.shopId = options.id;
|
||||
if (this.shopId) {
|
||||
// 从存储中读取用户位置信息
|
||||
this.loadUserLocation();
|
||||
this.loadStoreDetail();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "店铺信息错误",
|
||||
icon: "none",
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取系统信息
|
||||
|
||||
// 从存储中加载用户位置信息
|
||||
loadUserLocation() {
|
||||
const savedLocation = uni.getStorageSync("userLocation");
|
||||
if (savedLocation && savedLocation.latitude && savedLocation.longitude) {
|
||||
this.userLocation = savedLocation;
|
||||
}
|
||||
},
|
||||
|
||||
// 验证经纬度是否有效
|
||||
isValidCoordinate(lat, lng) {
|
||||
const latitude = parseFloat(lat);
|
||||
const longitude = parseFloat(lng);
|
||||
// 纬度范围:-90 到 90,经度范围:-180 到 180
|
||||
return (
|
||||
!isNaN(latitude) &&
|
||||
!isNaN(longitude) &&
|
||||
latitude >= -90 &&
|
||||
latitude <= 90 &&
|
||||
longitude >= -180 &&
|
||||
longitude <= 180
|
||||
);
|
||||
},
|
||||
|
||||
// 加载店铺详情
|
||||
async loadStoreDetail() {
|
||||
if (!this.shopId) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await getGuildStoreDetail(this.shopId);
|
||||
if (res) {
|
||||
this.storeInfo = res;
|
||||
// 设置地图中心点和标记
|
||||
if (res.latitude && res.longitude) {
|
||||
// 验证经纬度是否有效
|
||||
if (this.isValidCoordinate(res.latitude, res.longitude)) {
|
||||
// 直接设置地图中心点,避免从默认位置移动
|
||||
this.mapCenter = {
|
||||
latitude: parseFloat(res.latitude),
|
||||
longitude: parseFloat(res.longitude),
|
||||
};
|
||||
this.setMapMarkers();
|
||||
// 标记地图已初始化,此时再显示地图,避免移动动画
|
||||
this.$nextTick(() => {
|
||||
this.mapInitialized = true;
|
||||
});
|
||||
} else {
|
||||
console.warn("店铺经纬度无效:", res.latitude, res.longitude);
|
||||
uni.showToast({
|
||||
title: "店铺位置信息无效",
|
||||
icon: "none",
|
||||
});
|
||||
// 即使经纬度无效,也显示地图(使用默认位置)
|
||||
this.mapInitialized = true;
|
||||
}
|
||||
} else {
|
||||
// 如果没有经纬度,也显示地图(使用默认位置)
|
||||
this.mapInitialized = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载店铺详情失败:", error);
|
||||
uni.showToast({
|
||||
title: "加载店铺信息失败",
|
||||
icon: "none",
|
||||
});
|
||||
// 即使加载失败,也显示地图
|
||||
this.mapInitialized = true;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 设置地图标记点
|
||||
setMapMarkers() {
|
||||
if (!this.storeInfo.latitude || !this.storeInfo.longitude) return;
|
||||
|
||||
const lat = parseFloat(this.storeInfo.latitude);
|
||||
const lng = parseFloat(this.storeInfo.longitude);
|
||||
|
||||
// 再次验证经纬度有效性
|
||||
if (!this.isValidCoordinate(lat, lng)) {
|
||||
console.warn("标记点经纬度无效:", lat, lng);
|
||||
return;
|
||||
}
|
||||
|
||||
this.markers = [
|
||||
{
|
||||
id: 1,
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
title: this.storeInfo.name || "店铺位置",
|
||||
width: 30,
|
||||
height: 30,
|
||||
callout: {
|
||||
content: this.storeInfo.name || "店铺位置",
|
||||
color: "#333333",
|
||||
fontSize: 14,
|
||||
borderRadius: 4,
|
||||
bgColor: "#ffffff",
|
||||
padding: 8,
|
||||
display: "BYCLICK", // 点击时显示
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
// 计算两点之间的距离(单位:km)
|
||||
calculateDistance(lat1, lng1, lat2, lng2) {
|
||||
// 验证坐标有效性
|
||||
if (
|
||||
!this.isValidCoordinate(lat1, lng1) ||
|
||||
!this.isValidCoordinate(lat2, lng2)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const R = 6371; // 地球半径(km)
|
||||
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
||||
const dLng = ((lng2 - lng1) * Math.PI) / 180;
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos((lat1 * Math.PI) / 180) *
|
||||
Math.cos((lat2 * Math.PI) / 180) *
|
||||
Math.sin(dLng / 2) *
|
||||
Math.sin(dLng / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c;
|
||||
},
|
||||
|
||||
// // 进店按钮点击
|
||||
// handleEnterStore() {
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/detail/serviceDetail?id=${this.shopId}`,
|
||||
// });
|
||||
// },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.map-detail-page {
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 地图容器 */
|
||||
.map-container {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 店铺信息卡片 */
|
||||
.store-card {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
// height: 40vh;
|
||||
background-color: #ffffff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
|
||||
.store-card-content {
|
||||
display: flex;
|
||||
padding: 30rpx;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.store-image-wrapper {
|
||||
width: 186rpx;
|
||||
height: 200rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.store-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.store-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
.store-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.store-name {
|
||||
flex: 1;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
color: #1a1819;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.distance-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.location-icon {
|
||||
width: 17rpx;
|
||||
height: 20rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.distance-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 22rpx;
|
||||
color: #004294;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.store-detail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.5;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
|
||||
.detail-label {
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.store-tags-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.store-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex: 1;
|
||||
|
||||
.tag-item {
|
||||
padding: 7rpx 12rpx;
|
||||
font-size: 20rpx;
|
||||
border-radius: 4rpx;
|
||||
|
||||
&.tag-pink {
|
||||
background-color: rgba(213, 28, 60, 0.1);
|
||||
color: #d51c3c;
|
||||
border: 1rpx solid #d51c3c;
|
||||
}
|
||||
|
||||
&.tag-orange {
|
||||
background-color: rgba(255, 107, 0, 0.1);
|
||||
color: #ff6b00;
|
||||
border: 1rpx solid #ff6b00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enter-store-btn {
|
||||
align-self: flex-end;
|
||||
padding: 12rpx 40rpx;
|
||||
background: #004294;
|
||||
border-radius: 25rpx;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 26rpx;
|
||||
color: #ffffff;
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载中遮罩 */
|
||||
.loading-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
|
||||
.loading-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,607 @@
|
|||
<template>
|
||||
<view class="store-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<NavHeader title="店铺详情" />
|
||||
|
||||
<!-- 顶部店铺信息 -->
|
||||
<view class="store-header">
|
||||
<view class="store-brand">
|
||||
<image
|
||||
class="brand-image"
|
||||
:src="storeInfo.coverUrl || '/static/service/store-default.png'"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<view class="store-info">
|
||||
<text class="store-name">{{ storeInfo.name }}</text>
|
||||
<view class="store-detail">
|
||||
<text class="detail-text">门店地址: {{ storeInfo.address }}</text>
|
||||
</view>
|
||||
<view class="store-detail">
|
||||
<text class="detail-text">联系电话: {{ storeInfo.phone }}</text>
|
||||
</view>
|
||||
<view class="store-tags-row">
|
||||
<view class="store-tags">
|
||||
<view class="tag-item tag-pink">会员特惠</view>
|
||||
<view class="tag-item tag-orange">{{ categoryLabel }}</view>
|
||||
</view>
|
||||
<view class="distance-info">
|
||||
<image
|
||||
class="location-icon"
|
||||
src="/static/service/location-icon.png"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<text class="distance-text"
|
||||
>距您 {{ storeInfo.distance || "0" }}km</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中间菜单列表(可滑动) -->
|
||||
<scroll-view class="menu-list" scroll-y="true">
|
||||
<view class="menu-item" v-for="(item, index) in menuList" :key="index">
|
||||
<view
|
||||
class="checkbox"
|
||||
:class="{ checked: item.selected }"
|
||||
@click="toggleMenuItem(index)"
|
||||
>
|
||||
<text v-if="item.selected" class="checkmark">✓</text>
|
||||
</view>
|
||||
<image
|
||||
class="menu-image"
|
||||
:src="item.coverUrl || '/static/service/menu-default.png'"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="menu-info">
|
||||
<view class="menu-title-row">
|
||||
<text class="menu-title">{{ item.name }}</text>
|
||||
<view class="discount-badge" v-if="item.discount">
|
||||
{{ item.discount }}折
|
||||
</view>
|
||||
</view>
|
||||
<text class="menu-desc">{{ item.description }}</text>
|
||||
<view class="menu-price-row">
|
||||
<view class="current-price">
|
||||
<view class="price-label">现价¥</view>
|
||||
<view class="price-text">{{ item.salePrice || 0 }}</view>
|
||||
</view>
|
||||
<text class="original-price" v-if="item.originalPrice"
|
||||
>原价¥{{ item.originalPrice }}</text
|
||||
>
|
||||
<view class="quantity-control">
|
||||
<view
|
||||
class="quantity-btn minus"
|
||||
:class="{
|
||||
disabled: (item.quantity || 0) <= 0,
|
||||
}"
|
||||
@click="decreaseQuantity(index)"
|
||||
>-</view
|
||||
>
|
||||
<text class="quantity-number">{{ item.quantity || 0 }}</text>
|
||||
<view
|
||||
class="quantity-btn plus"
|
||||
@click="increaseQuantity(index)"
|
||||
>+</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部结算栏(固定) -->
|
||||
<view class="checkout-footer">
|
||||
<view class="total-info">
|
||||
<text class="total-label">总金额¥</text>
|
||||
<text class="total-amount">{{ totalAmount.toFixed(2) }}</text>
|
||||
<view class="member-benefit">
|
||||
<image
|
||||
class="crown-icon"
|
||||
src="/static/service/crown-icon.png"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<text class="benefit-text">{{ memberLevelName }}优惠</text>
|
||||
</view>
|
||||
</view>
|
||||
<button
|
||||
class="checkout-btn"
|
||||
:class="{ disabled: totalAmount <= 0 }"
|
||||
:disabled="totalAmount <= 0"
|
||||
@click="handleCheckout"
|
||||
>
|
||||
去结算
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getGuildStoreDetail, getGuildCoupon } from "@/api/service";
|
||||
import NavHeader from "@/components/NavHeader/NavHeader.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
storeInfo: {},
|
||||
menuList: [],
|
||||
categoryLabel: "",
|
||||
shopId: null,
|
||||
userInfo: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 计算总金额(只计算选中且数量大于0的商品)
|
||||
totalAmount() {
|
||||
return this.menuList.reduce((total, item) => {
|
||||
if (item.selected && item.quantity > 0) {
|
||||
const price = item.salePrice || item.currentPrice || item.price || 0;
|
||||
return total + price * item.quantity;
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
},
|
||||
// 是否有会员优惠
|
||||
hasMemberDiscount() {
|
||||
return this.menuList.some((item) => item.selected && item.discount);
|
||||
},
|
||||
// 获取会员等级名称(安全访问)
|
||||
memberLevelName() {
|
||||
return (this.userInfo && this.userInfo.level && this.userInfo.level.name) || '普通会员';
|
||||
},
|
||||
},
|
||||
onLoad(options) {
|
||||
// 从路由参数获取店铺ID和分类标签
|
||||
this.shopId = options.id;
|
||||
this.categoryLabel = options.categoryLabel;
|
||||
this.userInfo = uni.getStorageSync('userInfo') || {};
|
||||
if (this.shopId) {
|
||||
this.loadStoreData();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "店铺信息错误",
|
||||
icon: "none",
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载店铺数据
|
||||
async loadStoreData() {
|
||||
try {
|
||||
// 并行加载店铺详情和菜单
|
||||
await Promise.all([
|
||||
this.loadStoreDetail(this.shopId),
|
||||
this.loadStoreMenu(this.shopId),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("加载店铺数据失败:", error);
|
||||
uni.showToast({
|
||||
title: "加载店铺信息失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 加载店铺详情
|
||||
async loadStoreDetail(shopId) {
|
||||
try {
|
||||
const res = await getGuildStoreDetail(shopId);
|
||||
if (res) {
|
||||
this.storeInfo = res;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载店铺详情失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// 加载工会优惠券
|
||||
async loadStoreMenu(shopId) {
|
||||
try {
|
||||
const res = await getGuildCoupon({ shopId });
|
||||
console.log(res, 11119);
|
||||
if (res && res.list) {
|
||||
this.menuList = res.list;
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({
|
||||
title: error,
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 切换菜单项选择状态
|
||||
toggleMenuItem(index) {
|
||||
const item = this.menuList[index];
|
||||
item.selected = !item.selected;
|
||||
if (!item.selected) {
|
||||
item.quantity = 0;
|
||||
} else if (item.quantity === 0) {
|
||||
item.quantity = 1;
|
||||
}
|
||||
},
|
||||
|
||||
// 增加数量
|
||||
increaseQuantity(index) {
|
||||
const item = this.menuList[index];
|
||||
// 如果未选中,先选中
|
||||
if (!item.selected) {
|
||||
item.selected = true;
|
||||
}
|
||||
// 增加数量
|
||||
item.quantity = (item.quantity || 0) + 1;
|
||||
},
|
||||
|
||||
// 减少数量
|
||||
decreaseQuantity(index) {
|
||||
const item = this.menuList[index];
|
||||
const currentQuantity = item.quantity || 0;
|
||||
// 确保数量不能小于0
|
||||
if (currentQuantity > 0) {
|
||||
item.quantity = currentQuantity - 1;
|
||||
// 如果数量减到0,自动取消选中状态
|
||||
if (item.quantity === 0) {
|
||||
item.selected = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 去结算
|
||||
handleCheckout() {
|
||||
if (this.totalAmount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取选中的商品
|
||||
const selectedItems = this.menuList.filter(
|
||||
(item) => item.selected && item.quantity > 0
|
||||
);
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
uni.showToast({
|
||||
title: "请选择商品",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 跳转到结算页面或提交订单
|
||||
console.log("结算商品:", selectedItems);
|
||||
console.log("总金额:", this.totalAmount);
|
||||
|
||||
uni.showToast({
|
||||
title: `结算金额:¥${this.totalAmount.toFixed(2)}`,
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.store-page {
|
||||
min-height: 100vh;
|
||||
background-color: #e2e8f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 120rpx; // 为底部结算栏留出空间
|
||||
}
|
||||
|
||||
/* 顶部店铺信息 */
|
||||
.store-header {
|
||||
background-color: #ffffff;
|
||||
margin: 14rpx 20rpx;
|
||||
padding: 40rpx 30rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.store-brand {
|
||||
position: relative;
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
.brand-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.store-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.store-name {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
|
||||
.store-detail {
|
||||
.detail-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.store-tags-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8rpx;
|
||||
|
||||
.store-tags {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
|
||||
.tag-item {
|
||||
padding: 6rpx 16rpx;
|
||||
font-size: 20rpx;
|
||||
|
||||
&.tag-pink {
|
||||
background-color: rgba(213, 28, 60, 0.1);
|
||||
color: #d51c3c;
|
||||
}
|
||||
|
||||
&.tag-orange {
|
||||
background-color: rgba(255, 107, 0, 0.1);
|
||||
color: #ff6b00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.distance-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.location-icon {
|
||||
width: 17rpx;
|
||||
height: 20rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.distance-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #004294;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 中间菜单列表 */
|
||||
.menu-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
width: 95%;
|
||||
margin: 0 auto;
|
||||
min-height: 0;
|
||||
|
||||
.menu-item {
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 25rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.checkbox {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
border: 1rpx solid #cccccc;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 19rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.checked {
|
||||
background-color: #004294;
|
||||
border-color: #004294;
|
||||
|
||||
.checkmark {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-image {
|
||||
width: 160rpx;
|
||||
height: 173rpx;
|
||||
border-radius: 10rpx;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
.menu-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.menu-title {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.discount-badge {
|
||||
border: 1rpx solid #d51c3c;
|
||||
color: #d51c3c;
|
||||
font-size: 24rpx;
|
||||
padding: 6rpx 14rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-desc {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
line-height: 30rpx;
|
||||
}
|
||||
|
||||
.menu-price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10rpx;
|
||||
margin-top: 16rpx;
|
||||
|
||||
.current-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-weight: bold;
|
||||
font-size: 24rpx;
|
||||
color: #d51c3c;
|
||||
.price-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 50rpx;
|
||||
color: #d51c3c;
|
||||
}
|
||||
}
|
||||
|
||||
.original-price {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 20rpx;
|
||||
color: #545454;
|
||||
text-decoration: line-through;
|
||||
background-color: rgba(115, 115, 115, 0.1);
|
||||
padding: 7rpx 6rpx;
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-left: auto;
|
||||
|
||||
.quantity-btn {
|
||||
width: 35rpx;
|
||||
height: 35rpx;
|
||||
border: 1rpx solid #888888;
|
||||
line-height: 35rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
background-color: #ffffff;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.quantity-number {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
min-width: 40rpx;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部结算栏 */
|
||||
.checkout-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
padding: 25rpx 20rpx 25rpx 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-top: 1rpx solid #e2e8f1;
|
||||
z-index: 100;
|
||||
|
||||
.total-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.total-label {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 30rpx;
|
||||
color: #d51c3c;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 50rpx;
|
||||
color: #d51c3c;
|
||||
}
|
||||
|
||||
.member-benefit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-left: 12rpx;
|
||||
background: rgba(255, 107, 0, 0.1);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
|
||||
.crown-icon {
|
||||
width: 23rpx;
|
||||
height: 20rpx;
|
||||
}
|
||||
|
||||
.benefit-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 20rpx;
|
||||
color: #ff6b00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkout-btn {
|
||||
color: #ffffff;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
background: #004294;
|
||||
border-radius: 35rpx;
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -0,0 +1,577 @@
|
|||
<template>
|
||||
<view class="home-page">
|
||||
<!-- 应用头部 -->
|
||||
<view class="header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<image class="logo" src="/static/home/logo.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<!-- 招募宣传横幅 -->
|
||||
<swiper
|
||||
class="recruit-banner"
|
||||
:indicator-dots="bannerList.length > 1"
|
||||
:autoplay="bannerList.length > 1"
|
||||
:interval="3000"
|
||||
:duration="500"
|
||||
indicator-color="rgba(255, 255, 255, 0.5)"
|
||||
indicator-active-color="#ffffff"
|
||||
>
|
||||
<swiper-item
|
||||
v-for="(item, index) in bannerList"
|
||||
:key="index"
|
||||
@click="handleBannerClick(item)"
|
||||
>
|
||||
<image
|
||||
class="banner-image"
|
||||
:src="item.coverUrl"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 公会福利区 -->
|
||||
<view class="benefits-section">
|
||||
<view class="section-header">
|
||||
<view class="section-title">
|
||||
<image
|
||||
class="title-icon"
|
||||
src="/static/home/gift_icon.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="title-text">公会福利</text>
|
||||
</view>
|
||||
<view class="section-more" @click="handleViewAllBenefits">
|
||||
查看全部 >
|
||||
</view>
|
||||
</view>
|
||||
<view class="benefits-list">
|
||||
<view
|
||||
class="benefit-item"
|
||||
v-for="(item, index) in benefitsList"
|
||||
:key="index"
|
||||
@click="handleBenefitClick(item)"
|
||||
>
|
||||
<image class="benefit-icon" :src="item.icon" mode="aspectFit"></image>
|
||||
<text class="benefit-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 公会活动区 -->
|
||||
<view class="activities-section">
|
||||
<view class="section-header">
|
||||
<view class="section-title">
|
||||
<image
|
||||
class="title-icon"
|
||||
src="/static/home/activity_icon.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="title-text">公会活动</text>
|
||||
</view>
|
||||
<view class="section-more" @click="handleViewAllActivities">
|
||||
查看全部 >
|
||||
</view>
|
||||
</view>
|
||||
<view class="activities-list">
|
||||
<view
|
||||
class="activity-item"
|
||||
v-for="(item, index) in activitiesList"
|
||||
:key="index"
|
||||
@click="handleActivityClick(item)"
|
||||
>
|
||||
<view class="activity-image-wrapper">
|
||||
<image
|
||||
class="activity-image"
|
||||
:src="item.coverUrl"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<view class="activity-content">
|
||||
<view class="activity-title">{{ item.title }}</view>
|
||||
<view class="activity-desc">{{ item.summary }}</view>
|
||||
<view class="activity-time"
|
||||
>结束时间:{{ formatTime(item.endTime) }}</view
|
||||
>
|
||||
<view
|
||||
class="join-activity-btn"
|
||||
@click.stop="handleJoinActivity(item)"
|
||||
>
|
||||
<text>立即参与</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getHomeData } from "@/api/home.js";
|
||||
import {
|
||||
getGuildBenefits,
|
||||
getGuildActivities,
|
||||
joinGuild,
|
||||
joinActivity,
|
||||
} from "@/api/index.js";
|
||||
import { formatTime } from "@/utils/date.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// 状态栏高度
|
||||
statusBarHeight: 0,
|
||||
// 导航栏高度(状态栏 + 导航栏内容)
|
||||
navBarHeight: 0,
|
||||
bannerList: [],
|
||||
// 公会福利列表
|
||||
benefitsList: [
|
||||
{
|
||||
id: 1,
|
||||
label: "美食",
|
||||
icon: "/static/home/food_menu.png",
|
||||
type: "food",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: "保险",
|
||||
icon: "/static/home/car_insurance.png",
|
||||
type: "insurance",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: "维修",
|
||||
icon: "/static/home/repair.png",
|
||||
type: "repair",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: "健康",
|
||||
icon: "/static/home/health.png",
|
||||
type: "health",
|
||||
},
|
||||
],
|
||||
// 公会活动列表
|
||||
activitiesList: [],
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.getSystemInfo();
|
||||
this.loadHomeData();
|
||||
this.loadActivities();
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadHomeData().finally(() => {
|
||||
uni.stopPullDownRefresh();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
// 获取系统信息
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || 0;
|
||||
|
||||
// 导航栏高度 = 状态栏高度 + 导航栏内容高度(通常44px,但不同平台可能不同)
|
||||
// 这里可以根据实际需求调整导航栏内容高度
|
||||
const navBarContentHeight = 44; // 导航栏内容高度,可根据设计稿调整
|
||||
this.navBarHeight = this.statusBarHeight + navBarContentHeight;
|
||||
},
|
||||
// 加载首页数据
|
||||
async loadHomeData() {
|
||||
try {
|
||||
// 获取首页banner
|
||||
const res = await getHomeData({ noticeType: 1 });
|
||||
if (res) {
|
||||
this.bannerList = res.list || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载首页数据失败:", error);
|
||||
uni.showToast({
|
||||
title: "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 加载公会福利
|
||||
async loadBenefits() {
|
||||
try {
|
||||
// 后续补充接口调用
|
||||
// const res = await getGuildBenefits()
|
||||
// if (res.statusCode === 200) {
|
||||
// this.benefitsList = res.data || this.benefitsList
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error("加载公会福利失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// 加载公会活动
|
||||
async loadActivities() {
|
||||
try {
|
||||
// 后续补充接口调用
|
||||
const res = await getHomeData({ page: 1, pageSize: 3, noticeType: 2 });
|
||||
if (res) {
|
||||
this.activitiesList = res.list || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载公会活动失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化时间戳
|
||||
formatTime,
|
||||
|
||||
// 菜单点击
|
||||
handleMenuClick() {
|
||||
uni.showToast({
|
||||
title: "菜单功能",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
|
||||
// 聚焦/关注点击
|
||||
handleFocusClick() {
|
||||
uni.showToast({
|
||||
title: "关注功能",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
|
||||
// banner点击跳转
|
||||
handleBannerClick(item) {
|
||||
if (item.jumpUrl) {
|
||||
// 判断跳转类型
|
||||
if (
|
||||
item.jumpUrl.startsWith("http://") ||
|
||||
item.jumpUrl.startsWith("https://")
|
||||
) {
|
||||
// 外部链接,使用web-view或浏览器打开
|
||||
// #ifdef H5
|
||||
window.open(item.jumpUrl);
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.navigateTo({
|
||||
url: `/pages/webview/webview?url=${encodeURIComponent(
|
||||
item.jumpUrl
|
||||
)}`,
|
||||
});
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openURL(item.jumpUrl);
|
||||
// #endif
|
||||
} else {
|
||||
// 内部页面跳转
|
||||
uni.navigateTo({
|
||||
url: item.jumpUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 加入工会
|
||||
async handleJoinGuild() {
|
||||
try {
|
||||
// 后续补充接口调用
|
||||
// const res = await joinGuild()
|
||||
// if (res.statusCode === 200) {
|
||||
// uni.showToast({
|
||||
// title: '加入成功',
|
||||
// icon: 'success'
|
||||
// })
|
||||
// }
|
||||
uni.showToast({
|
||||
title: "加入工会",
|
||||
icon: "none",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加入工会失败:", error);
|
||||
uni.showToast({
|
||||
title: "操作失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 查看全部福利
|
||||
handleViewAllBenefits() {
|
||||
// 清除分类信息,跳转到服务页面显示全部
|
||||
const app = getApp();
|
||||
if (app && app.globalData) {
|
||||
app.globalData.serviceCategory = null;
|
||||
}
|
||||
uni.switchTab({
|
||||
url: '/pages/service/service'
|
||||
});
|
||||
},
|
||||
|
||||
// 福利项点击
|
||||
handleBenefitClick(item) {
|
||||
// 建立 type 到 value 的映射关系
|
||||
const typeToValueMap = {
|
||||
food: '0', // 美食
|
||||
insurance: '4', // 保险
|
||||
repair: '1', // 维修
|
||||
health: '5' // 健康
|
||||
};
|
||||
|
||||
// 获取对应的分类 value
|
||||
const categoryValue = typeToValueMap[item.type];
|
||||
|
||||
console.log(categoryValue, 222);
|
||||
|
||||
// 将分类信息存储到 globalData,供 service 页面使用
|
||||
const app = getApp();
|
||||
if (app && app.globalData && categoryValue) {
|
||||
app.globalData.serviceCategory = categoryValue;
|
||||
}
|
||||
|
||||
// 跳转到服务页面
|
||||
uni.switchTab({
|
||||
url: '/pages/service/service'
|
||||
});
|
||||
},
|
||||
|
||||
// 查看全部活动
|
||||
handleViewAllActivities() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/activities/list'
|
||||
});
|
||||
},
|
||||
|
||||
// 活动项点击
|
||||
handleActivityClick(item) {
|
||||
// 后续可以跳转到活动详情页
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/activities/detail?id=${item.id}`
|
||||
// })
|
||||
},
|
||||
|
||||
// 参与活动
|
||||
async handleJoinActivity(item) {
|
||||
// 跳转到活动详情页
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/activitiesDetail?id=${item.id}`
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background-color: #e2e8f1;
|
||||
padding-bottom: 100rpx; // 为底部tabBar留出空间
|
||||
}
|
||||
|
||||
/* 应用头部 */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 34rpx;
|
||||
|
||||
.logo {
|
||||
width: 306rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 招募宣传横幅 */
|
||||
.recruit-banner {
|
||||
padding: 32rpx 20rpx 20rpx 20rpx;
|
||||
height: 398rpx;
|
||||
|
||||
.banner-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 通用区块样式 */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 27rpx;
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.title-icon {
|
||||
width: 51rpx;
|
||||
height: 51rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-family: AlibabaPuHuiTi, AlibabaPuHuiTi;
|
||||
font-weight: 500;
|
||||
font-size: 36rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
/* 公会福利区 */
|
||||
.benefits-section {
|
||||
margin: 0 20rpx;
|
||||
padding: 30rpx 22rpx 27rpx 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx 20rpx 20rpx 20rpx;
|
||||
|
||||
.benefits-list {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0 30rpx;
|
||||
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.benefit-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 16rpx;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.benefit-item:active .benefit-icon {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.benefit-label {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 公会活动区 */
|
||||
.activities-section {
|
||||
margin: 20rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx 20rpx 20rpx 20rpx;
|
||||
padding: 32rpx 20rpx 62rpx 20rpx;
|
||||
|
||||
.activities-list {
|
||||
.activity-item {
|
||||
display: flex;
|
||||
margin-bottom: 30rpx;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.activity-image-wrapper {
|
||||
width: 186rpx;
|
||||
height: 200rpx;
|
||||
flex-shrink: 0;
|
||||
border: 1rpx solid #e2e8f1;
|
||||
border-radius: 10rpx;
|
||||
|
||||
.activity-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.activity-image-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0.1),
|
||||
rgba(0, 0, 0, 0.4)
|
||||
);
|
||||
padding: 20rpx;
|
||||
|
||||
.overlay-text {
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
||||
.activity-title {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
color: #1a1819;
|
||||
margin-bottom: 12rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
width: 260rpx;
|
||||
height: 33rpx;
|
||||
padding: 0 10rpx;
|
||||
background: rgba(0, 66, 148, 0.1);
|
||||
color: #004294;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.join-activity-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #004294;
|
||||
border-radius: 25rpx;
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,523 @@
|
|||
<template>
|
||||
<view class="login-page">
|
||||
<!-- 状态栏占位 -->
|
||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
|
||||
<!-- 头部区域 -->
|
||||
<view class="header">
|
||||
<view class="back-btn" @click="handleBack" v-if="showBack">
|
||||
<text class="back-icon">‹</text>
|
||||
</view>
|
||||
<view class="header-title">登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录内容区 -->
|
||||
<view class="login-content">
|
||||
<!-- Logo区域 -->
|
||||
<!-- <view class="logo-section">
|
||||
<image class="logo" src="/static/logo.png" mode="aspectFit"></image>
|
||||
</view> -->
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<view class="login-form">
|
||||
<!-- 手机号输入 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">手机号</text>
|
||||
<input
|
||||
class="input-field"
|
||||
type="number"
|
||||
v-model="formData.mobile"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">密码</text>
|
||||
<input
|
||||
class="input-field"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
v-model="formData.password"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
<view class="password-toggle" @click="togglePassword">
|
||||
<text class="toggle-icon">{{ showPassword ? '👁️' : '👁️🗨️' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
class="login-btn"
|
||||
:class="{ disabled: !canLogin }"
|
||||
:disabled="!canLogin || loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<view class="divider">
|
||||
<view class="divider-line"></view>
|
||||
<text class="divider-text">或</text>
|
||||
<view class="divider-line"></view>
|
||||
</view>
|
||||
|
||||
<!-- 小程序一键授权登录 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button
|
||||
class="wechat-login-btn"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="handleGetPhoneNumber"
|
||||
:disabled="loading"
|
||||
>
|
||||
<text>一键授权手机号快速登录</text>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="wechat-login-tip">
|
||||
<text>小程序一键授权登录仅支持微信小程序环境</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { login, loginByPhone } from '@/api/auth.js'
|
||||
import { getUserInfo } from '@/api/profile.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 0,
|
||||
showBack: false,
|
||||
loading: false,
|
||||
showPassword: false,
|
||||
formData: {
|
||||
mobile: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 判断是否可以登录
|
||||
canLogin() {
|
||||
return (
|
||||
this.formData.mobile.length === 11 &&
|
||||
this.formData.password.length > 0
|
||||
)
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 检查是否已登录
|
||||
const token = uni.getStorageSync('token')
|
||||
if (token) {
|
||||
// 已登录,跳转到首页
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index',
|
||||
fail: () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 判断是否显示返回按钮(从其他页面跳转过来时显示)
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
this.showBack = true
|
||||
}
|
||||
this.getSystemInfo()
|
||||
},
|
||||
methods: {
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || 0
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
handleBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
|
||||
// 切换密码显示/隐藏
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword
|
||||
},
|
||||
|
||||
// 账户密码登录
|
||||
async handleLogin() {
|
||||
if (!this.canLogin) {
|
||||
uni.showToast({
|
||||
title: '请填写完整的登录信息',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const mobileReg = /^1[3-9]\d{9}$/
|
||||
if (!mobileReg.test(this.formData.mobile)) {
|
||||
uni.showToast({
|
||||
title: '请输入正确的手机号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await login({
|
||||
mobile: this.formData.mobile,
|
||||
password: this.formData.password
|
||||
})
|
||||
|
||||
// 登录成功,保存token(兼容不同的响应格式)
|
||||
const token = res?.accessToken || res?.token || res?.data?.accessToken || res?.data?.token
|
||||
if (token) {
|
||||
uni.setStorageSync('token', token)
|
||||
}
|
||||
|
||||
// 保存refreshToken(用于刷新accessToken)
|
||||
const refreshToken = res?.refreshToken || res?.data?.refreshToken
|
||||
if (refreshToken) {
|
||||
uni.setStorageSync('refreshToken', refreshToken)
|
||||
}
|
||||
|
||||
// 保存token过期时间
|
||||
const expiresTime = res?.expiresTime || res?.data?.expiresTime
|
||||
if (expiresTime) {
|
||||
uni.setStorageSync('tokenExpiresTime', expiresTime)
|
||||
}
|
||||
|
||||
// 保存用户ID
|
||||
const userId = res?.userId || res?.data?.userId
|
||||
if (userId) {
|
||||
uni.setStorageSync('userId', userId)
|
||||
}
|
||||
|
||||
// 保存用户信息(如果有)
|
||||
const userInfo = res?.userInfo || res?.data?.userInfo || res?.user || res?.data?.user
|
||||
if (userInfo) {
|
||||
uni.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
|
||||
// 登录成功后,调用个人信息接口获取完整用户信息
|
||||
try {
|
||||
const userInfoRes = await getUserInfo()
|
||||
if (userInfoRes && userInfoRes.data) {
|
||||
// 根据接口返回的数据结构,保存 res.data
|
||||
uni.setStorageSync('userInfo', userInfoRes.data)
|
||||
} else if (userInfoRes) {
|
||||
// 兼容直接返回数据的情况
|
||||
uni.setStorageSync('userInfo', userInfoRes)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
// 获取用户信息失败不影响登录流程,继续执行
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟跳转,让用户看到成功提示
|
||||
setTimeout(() => {
|
||||
// 检查是否有返回路径
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
// 有上一页,返回上一页
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
// 没有上一页,跳转到首页
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
// 错误信息已在request中统一处理,这里不需要再次提示
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 小程序一键授权手机号登录
|
||||
async handleGetPhoneNumber(e) {
|
||||
console.log('获取手机号授权结果:', e)
|
||||
|
||||
if (e.detail.errMsg === 'getPhoneNumber:ok') {
|
||||
this.loading = true
|
||||
try {
|
||||
// 获取微信登录code(用于后端解密手机号)
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
// 调用登录接口
|
||||
// 注意:这里需要根据后端实际接口要求传递参数
|
||||
// 通常需要传递 code、encryptedData、iv 等参数
|
||||
const res = await loginByPhone({
|
||||
code: loginRes.code,
|
||||
encryptedData: e.detail.encryptedData,
|
||||
iv: e.detail.iv
|
||||
})
|
||||
|
||||
// 登录成功,保存token(兼容不同的响应格式)
|
||||
const token = res?.token || res?.data?.token || res?.accessToken || res?.access_token
|
||||
if (token) {
|
||||
uni.setStorageSync('token', token)
|
||||
}
|
||||
|
||||
// 保存refreshToken(用于刷新accessToken)
|
||||
const refreshToken = res?.refreshToken || res?.data?.refreshToken
|
||||
if (refreshToken) {
|
||||
uni.setStorageSync('refreshToken', refreshToken)
|
||||
}
|
||||
|
||||
// 保存token过期时间
|
||||
const expiresTime = res?.expiresTime || res?.data?.expiresTime
|
||||
if (expiresTime) {
|
||||
uni.setStorageSync('tokenExpiresTime', expiresTime)
|
||||
}
|
||||
|
||||
// 保存用户ID
|
||||
const userId = res?.userId || res?.data?.userId
|
||||
if (userId) {
|
||||
uni.setStorageSync('userId', userId)
|
||||
}
|
||||
|
||||
// 保存用户信息(如果有)
|
||||
const userInfo = res?.userInfo || res?.data?.userInfo || res?.user || res?.data?.user
|
||||
if (userInfo) {
|
||||
uni.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
|
||||
// 登录成功后,调用个人信息接口获取完整用户信息
|
||||
try {
|
||||
const userInfoRes = await getUserInfo()
|
||||
uni.setStorageSync('userInfo', userInfoRes)
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟跳转
|
||||
setTimeout(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
console.error('一键登录失败:', error)
|
||||
// 错误信息已在request中统一处理
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '授权失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #e2e8f1 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 20rpx 0;
|
||||
background-color: transparent;
|
||||
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
left: 30rpx;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
.back-icon {
|
||||
font-size: 60rpx;
|
||||
color: #333333;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 500;
|
||||
color: #1a1819;
|
||||
}
|
||||
}
|
||||
|
||||
.login-content {
|
||||
padding: 60rpx 60rpx 0;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 80rpx;
|
||||
|
||||
.logo {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.form-item {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.input-label {
|
||||
width: 120rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
width: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #004294 0%, #0066cc 100%);
|
||||
border-radius: 44rpx;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
margin-top: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #cccccc;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 60rpx 0 40rpx;
|
||||
padding: 0 20rpx;
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1rpx;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
margin: 0 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background-color: #07c160;
|
||||
border-radius: 44rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.wechat-login-tip {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,456 @@
|
|||
<template>
|
||||
<view class="profile-page">
|
||||
<view class="header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<text class="header-text">个人中心</text>
|
||||
</view>
|
||||
|
||||
<!-- 用户资料卡片 -->
|
||||
<view
|
||||
class="user-card"
|
||||
:style="{
|
||||
backgroundImage: userInfo.level.backgroundUrl
|
||||
? `url(${userInfo.level.backgroundUrl})`
|
||||
: 'url(/static/profile/member-card-bg.png)',
|
||||
}"
|
||||
>
|
||||
<view class="user-info">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="userInfo.avatar || '/static/logo.png'"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<image
|
||||
src="/static/profile/avatar-hg.png"
|
||||
mode="aspectFill"
|
||||
class="avatar-hg"
|
||||
></image>
|
||||
<view class="user-details">
|
||||
<text class="login-method">{{
|
||||
userInfo.nickname || "微信登录"
|
||||
}}</text>
|
||||
<text class="user-id">NO.{{ userInfo.id }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-benefits-btn" @click="goToMemberBenefits">
|
||||
<image :src="userInfo.level.icon" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="user-notice">
|
||||
<!-- <text class="user-notice-text">{{ noticeNum }}</text> -->
|
||||
<image src="/static/profile/user-notice.png" mode="aspectFill"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能卡片 -->
|
||||
<view class="action-cards">
|
||||
<view class="action-card real-name-auth" @click="goToRealNameAuth">
|
||||
<view class="card-content">
|
||||
<text class="card-title">实名认证</text>
|
||||
<text class="card-desc">请完善身份信息></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-card service-records" @click="goToServiceRecords">
|
||||
<view class="card-content">
|
||||
<text class="card-title">服务记录</text>
|
||||
<text class="card-desc">查看服务记录></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="menu-list">
|
||||
<view class="menu-item" @click="goToFavorites">
|
||||
<view class="menu-icon"
|
||||
><image
|
||||
src="/static/profile/my-favorite.png"
|
||||
mode="aspectFill"
|
||||
></image
|
||||
></view>
|
||||
<text class="menu-text">我的收藏</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToComplaints">
|
||||
<view class="menu-icon"
|
||||
><image src="/static/profile/complaints.png" mode="aspectFill"></image
|
||||
></view>
|
||||
<text class="menu-text">投诉建议</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goToPostMessage">
|
||||
<view class="menu-icon"
|
||||
><image
|
||||
src="/static/profile/publish-message.png"
|
||||
mode="aspectFill"
|
||||
></image
|
||||
></view>
|
||||
<text class="menu-text">发布消息</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录按钮 -->
|
||||
<view class="logout-section">
|
||||
<button class="logout-btn" @click="handleLogout">退出登录</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserInfo } from "@/api/profile.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 0,
|
||||
navBarHeight: 0,
|
||||
currentTime: "10:55",
|
||||
userInfo: {},
|
||||
noticeNum: 3,
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.getSystemInfo();
|
||||
this.updateTime();
|
||||
// 先加载本地存储的用户信息,然后刷新
|
||||
this.loadUserInfo();
|
||||
},
|
||||
onShow() {
|
||||
// 页面显示时刷新用户信息(可选,如果不需要每次都刷新可以注释掉)
|
||||
// this.loadUserInfo();
|
||||
},
|
||||
methods: {
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
// 获取系统信息
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
// 获取状态栏高度,如果获取不到则根据平台设置默认值
|
||||
// iOS通常为44px(iPhone X及以后)或20px(iPhone 8及以前)
|
||||
// Android通常为24px或更高
|
||||
let defaultHeight = 20;
|
||||
// #ifdef APP-PLUS
|
||||
if (systemInfo.platform === "ios") {
|
||||
// iPhone X及以后机型状态栏高度为44px
|
||||
defaultHeight = systemInfo.screenHeight >= 812 ? 44 : 20;
|
||||
} else {
|
||||
defaultHeight = 24;
|
||||
}
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
defaultHeight = 0; // H5环境通常不需要状态栏高度
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
defaultHeight = systemInfo.statusBarHeight || 20; // 小程序环境
|
||||
// #endif
|
||||
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || defaultHeight;
|
||||
const navBarContentHeight = 44; // 导航栏内容高度,可根据设计稿调整
|
||||
this.navBarHeight = this.statusBarHeight + navBarContentHeight;
|
||||
|
||||
console.log(
|
||||
"状态栏高度:",
|
||||
this.statusBarHeight,
|
||||
"平台:",
|
||||
systemInfo.platform
|
||||
);
|
||||
},
|
||||
updateTime() {
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, "0");
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
this.currentTime = `${hours}:${minutes}`;
|
||||
// 每分钟更新一次时间
|
||||
setInterval(() => {
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, "0");
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
this.currentTime = `${hours}:${minutes}`;
|
||||
}, 60000);
|
||||
},
|
||||
async loadUserInfo() {
|
||||
try {
|
||||
// 先尝试从本地存储获取,立即显示(避免等待接口响应)
|
||||
const localUserInfo = uni.getStorageSync("userInfo");
|
||||
if (localUserInfo) {
|
||||
this.userInfo = localUserInfo;
|
||||
}
|
||||
|
||||
// 调用接口获取最新用户信息(登录时已调用,这里主要是刷新)
|
||||
const res = await getUserInfo();
|
||||
|
||||
// 更新用户信息
|
||||
this.userInfo = res;
|
||||
// 保存到本地存储
|
||||
uni.setStorageSync("userInfo", this.userInfo);
|
||||
} catch (error) {
|
||||
console.error("获取用户信息失败:", error);
|
||||
// 如果接口调用失败,使用本地存储的数据(登录时已保存)
|
||||
const localUserInfo = uni.getStorageSync("userInfo");
|
||||
if (localUserInfo) {
|
||||
this.userInfo = localUserInfo;
|
||||
}
|
||||
}
|
||||
},
|
||||
goToMemberBenefits() {
|
||||
uni.showToast({
|
||||
title: "会员权益",
|
||||
icon: "none",
|
||||
});
|
||||
},
|
||||
goToRealNameAuth() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/profile/realNameAuth",
|
||||
});
|
||||
},
|
||||
goToServiceRecords() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/profile/serviceRecords",
|
||||
});
|
||||
},
|
||||
goToFavorites() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/activities/myCollect",
|
||||
});
|
||||
},
|
||||
goToComplaints() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/activities/complaints",
|
||||
});
|
||||
},
|
||||
goToPostMessage() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/activities/postMessage",
|
||||
});
|
||||
},
|
||||
handleLogout() {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要退出登录吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 清除所有登录信息
|
||||
uni.removeStorageSync("token");
|
||||
uni.removeStorageSync("refreshToken");
|
||||
uni.removeStorageSync("tokenExpiresTime");
|
||||
uni.removeStorageSync("userId");
|
||||
uni.removeStorageSync("userInfo");
|
||||
uni.reLaunch({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: radial-gradient(0% 0% at 0% 0%, #ffffff 0%, #e2e8f1 100%);
|
||||
padding-bottom: 120rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
.header-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 34rpx;
|
||||
color: #000000;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 用户资料卡片 */
|
||||
.user-card {
|
||||
margin: 10rpx 10rpx 22rpx 10rpx;
|
||||
height: 289rpx;
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 89rpx;
|
||||
padding-left: 23rpx;
|
||||
.avatar {
|
||||
width: 109rpx;
|
||||
height: 108rpx;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
.avatar-hg {
|
||||
width: 51rpx;
|
||||
height: 54rpx;
|
||||
position: absolute;
|
||||
top: 72rpx;
|
||||
left: 103rpx;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
|
||||
.login-method {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.member-benefits-btn {
|
||||
position: absolute;
|
||||
right: 50rpx;
|
||||
bottom: 24rpx;
|
||||
width: 175rpx;
|
||||
height: 42rpx;
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.user-notice {
|
||||
position: absolute;
|
||||
right: 44rpx;
|
||||
top: 31rpx;
|
||||
.user-notice-text {
|
||||
display: block;
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
background: #d51c3c;
|
||||
border: 2rpx solid #e2e8f1;
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
line-height: 22rpx;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
image {
|
||||
width: 24rpx;
|
||||
height: 29rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 功能卡片 */
|
||||
.action-cards {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
margin: 0 20rpx 27rpx 20rpx;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
height: 124rpx;
|
||||
.card-content {
|
||||
padding: 41rpx 36rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
|
||||
.card-title {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
color: #3c454c;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #ba9666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.real-name-auth {
|
||||
background-image: url("https://resource2.ctshenglong.cn/20251219/实名认证_1766107765758.png");
|
||||
}
|
||||
.service-records {
|
||||
background-image: url("https://resource2.ctshenglong.cn/20251219/服务记录_1766107759755.png");
|
||||
}
|
||||
|
||||
/* 菜单列表 */
|
||||
.menu-list {
|
||||
margin: 0 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 29rpx 36rpx 28rpx 20rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 11rpx;
|
||||
|
||||
.menu-icon {
|
||||
width: 43rpx;
|
||||
height: 43rpx;
|
||||
margin-right: 24rpx;
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 40rpx;
|
||||
color: #cccccc;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 退出登录 */
|
||||
.logout-section {
|
||||
margin-top: 132rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 85%;
|
||||
height: 80rpx;
|
||||
background: #004294;
|
||||
border-radius: 39rpx;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -0,0 +1,579 @@
|
|||
<template>
|
||||
<view class="real-name-auth-page">
|
||||
<!-- 头部区域 -->
|
||||
<NavHeader title="实名认证" />
|
||||
|
||||
<!-- 表单内容区 -->
|
||||
<scroll-view class="form-content" scroll-y="true">
|
||||
<!-- 基本信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">基本信息</view>
|
||||
|
||||
<!-- 真实姓名 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">真实姓名</text>
|
||||
<input
|
||||
class="input-field"
|
||||
type="text"
|
||||
v-model="formData.realName"
|
||||
placeholder="请输入真实姓名"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 证件类型 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">证件类型</text>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="idTypeOptions"
|
||||
range-key="label"
|
||||
:value="idTypeIndex"
|
||||
@change="handleIdTypeChange"
|
||||
>
|
||||
<view class="picker-view">
|
||||
<text :class="['picker-text', !formData.idType ? 'placeholder' : '']">
|
||||
{{ formData.idType ? idTypeOptions.find(item => item.value === formData.idType)?.label : '请选择证件类型' }}
|
||||
</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 证件号码 -->
|
||||
<view class="form-item">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-label">证件号码</text>
|
||||
<input
|
||||
class="input-field"
|
||||
type="text"
|
||||
v-model="formData.idNumber"
|
||||
placeholder="请输入证件号码"
|
||||
maxlength="30"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 身份证照片 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">身份证照片</view>
|
||||
|
||||
<!-- 身份证正面 -->
|
||||
<view class="form-item">
|
||||
<view class="upload-wrapper">
|
||||
<text class="upload-label">身份证正面</text>
|
||||
<view class="upload-box" @click="chooseImage('idFrontImg')">
|
||||
<image
|
||||
v-if="formData.idFrontImg"
|
||||
class="uploaded-image"
|
||||
:src="formData.idFrontImg"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 身份证背面 -->
|
||||
<view class="form-item">
|
||||
<view class="upload-wrapper">
|
||||
<text class="upload-label">身份证背面</text>
|
||||
<view class="upload-box" @click="chooseImage('idBackImg')">
|
||||
<image
|
||||
v-if="formData.idBackImg"
|
||||
class="uploaded-image"
|
||||
:src="formData.idBackImg"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 驾驶证照片 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">驾驶证照片</view>
|
||||
|
||||
<!-- 驾驶证正面 -->
|
||||
<view class="form-item">
|
||||
<view class="upload-wrapper">
|
||||
<text class="upload-label">驾驶证正面</text>
|
||||
<view class="upload-box" @click="chooseImage('driverLicenseFrontImg')">
|
||||
<image
|
||||
v-if="formData.driverLicenseFrontImg"
|
||||
class="uploaded-image"
|
||||
:src="formData.driverLicenseFrontImg"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 驾驶证背面 -->
|
||||
<view class="form-item">
|
||||
<view class="upload-wrapper">
|
||||
<text class="upload-label">驾驶证背面</text>
|
||||
<view class="upload-box" @click="chooseImage('driverLicenseBackImg')">
|
||||
<image
|
||||
v-if="formData.driverLicenseBackImg"
|
||||
class="uploaded-image"
|
||||
:src="formData.driverLicenseBackImg"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 行驶证照片 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">行驶证照片</view>
|
||||
|
||||
<!-- 行驶证正面 -->
|
||||
<view class="form-item">
|
||||
<view class="upload-wrapper">
|
||||
<text class="upload-label">行驶证正面</text>
|
||||
<view class="upload-box" @click="chooseImage('vehicleLicenseFrontImg')">
|
||||
<image
|
||||
v-if="formData.vehicleLicenseFrontImg"
|
||||
class="uploaded-image"
|
||||
:src="formData.vehicleLicenseFrontImg"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 行驶证背面 -->
|
||||
<view class="form-item">
|
||||
<view class="upload-wrapper">
|
||||
<text class="upload-label">行驶证背面</text>
|
||||
<view class="upload-box" @click="chooseImage('vehicleLicenseBackImg')">
|
||||
<image
|
||||
v-if="formData.vehicleLicenseBackImg"
|
||||
class="uploaded-image"
|
||||
:src="formData.vehicleLicenseBackImg"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view v-else class="upload-placeholder">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注信息 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">备注信息</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="textarea-wrapper">
|
||||
<textarea
|
||||
class="textarea-field"
|
||||
v-model="formData.imgVerifyRemark"
|
||||
placeholder="请输入备注信息(选填)"
|
||||
maxlength="200"
|
||||
:auto-height="true"
|
||||
></textarea>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="submit-section">
|
||||
<button
|
||||
class="submit-btn"
|
||||
:class="{ disabled: loading }"
|
||||
:disabled="loading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? '提交中...' : '提交认证' }}
|
||||
</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { createRealNameInfo } from '@/api/profile.js'
|
||||
import NavHeader from "@/components/NavHeader/NavHeader.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
idTypeIndex: 0,
|
||||
idTypeOptions: [
|
||||
{ label: '身份证', value: 1 },
|
||||
{ label: '其他', value: 2 }
|
||||
],
|
||||
formData: {
|
||||
realName: '',
|
||||
idType: null,
|
||||
idNumber: '',
|
||||
idFrontImg: '',
|
||||
idBackImg: '',
|
||||
driverLicenseFrontImg: '',
|
||||
driverLicenseBackImg: '',
|
||||
vehicleLicenseFrontImg: '',
|
||||
vehicleLicenseBackImg: '',
|
||||
imgVerifyRemark: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
},
|
||||
methods: {
|
||||
|
||||
// 证件类型选择
|
||||
handleIdTypeChange(e) {
|
||||
this.idTypeIndex = e.detail.value
|
||||
this.formData.idType = this.idTypeOptions[e.detail.value].value
|
||||
},
|
||||
|
||||
// 选择图片
|
||||
chooseImage(field) {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const tempFilePath = res.tempFilePaths[0]
|
||||
// 先显示本地预览
|
||||
this.formData[field] = tempFilePath
|
||||
// 上传图片到服务器
|
||||
this.uploadImage(tempFilePath, field)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择图片失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择图片失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 上传图片
|
||||
uploadImage(filePath, field) {
|
||||
uni.showLoading({
|
||||
title: '上传中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 获取token
|
||||
const token = uni.getStorageSync('token')
|
||||
const BASE_URL = 'https://siji.chenjuncn.top'
|
||||
|
||||
uni.uploadFile({
|
||||
url: `${BASE_URL}/app-api/infra/file/upload`,
|
||||
filePath: filePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'tenant-id': '1'
|
||||
},
|
||||
success: (res) => {
|
||||
uni.hideLoading()
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 200 || data.code === 0) {
|
||||
// 上传成功,保存图片URL
|
||||
const imageUrl = data.data?.url || data.data || data.url
|
||||
if (imageUrl) {
|
||||
this.formData[field] = imageUrl
|
||||
uni.showToast({
|
||||
title: '上传成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
} else {
|
||||
throw new Error('上传成功但未返回图片地址')
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || data.msg || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析上传结果失败:', error)
|
||||
uni.showToast({
|
||||
title: '上传失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
// 上传失败,清除预览
|
||||
this.formData[field] = ''
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('上传图片失败:', err)
|
||||
uni.showToast({
|
||||
title: '上传失败,请检查网络',
|
||||
icon: 'none'
|
||||
})
|
||||
// 上传失败,清除预览
|
||||
this.formData[field] = ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 提交表单
|
||||
async handleSubmit() {
|
||||
// 基本验证
|
||||
if (!this.formData.realName || !this.formData.realName.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入真实姓名',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.formData.idType) {
|
||||
uni.showToast({
|
||||
title: '请选择证件类型',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.formData.idNumber || !this.formData.idNumber.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入证件号码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
// 构建提交数据,只包含有值的字段
|
||||
const submitData = {}
|
||||
if (this.formData.realName) submitData.realName = this.formData.realName.trim()
|
||||
if (this.formData.idType !== null) submitData.idType = this.formData.idType
|
||||
if (this.formData.idNumber) submitData.idNumber = this.formData.idNumber.trim()
|
||||
if (this.formData.idFrontImg) submitData.idFrontImg = this.formData.idFrontImg
|
||||
if (this.formData.idBackImg) submitData.idBackImg = this.formData.idBackImg
|
||||
if (this.formData.driverLicenseFrontImg) submitData.driverLicenseFrontImg = this.formData.driverLicenseFrontImg
|
||||
if (this.formData.driverLicenseBackImg) submitData.driverLicenseBackImg = this.formData.driverLicenseBackImg
|
||||
if (this.formData.vehicleLicenseFrontImg) submitData.vehicleLicenseFrontImg = this.formData.vehicleLicenseFrontImg
|
||||
if (this.formData.vehicleLicenseBackImg) submitData.vehicleLicenseBackImg = this.formData.vehicleLicenseBackImg
|
||||
if (this.formData.imgVerifyRemark) submitData.imgVerifyRemark = this.formData.imgVerifyRemark.trim()
|
||||
|
||||
await createRealNameInfo(submitData)
|
||||
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
// 错误信息已在request中统一处理
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.real-name-auth-page {
|
||||
min-height: 100vh;
|
||||
background: #e2e8f1;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
height: calc(100vh - 120rpx);
|
||||
padding: 0 30rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 24rpx;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.input-label {
|
||||
width: 160rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
|
||||
&.placeholder {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 40rpx;
|
||||
color: #cccccc;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-wrapper {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.upload-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
border: 2rpx dashed #d0d0d0;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fafafa;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.uploaded-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
|
||||
.upload-icon {
|
||||
font-size: 60rpx;
|
||||
color: #999999;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.textarea-field {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1a1819;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
margin-top: 40rpx;
|
||||
padding-bottom: 40rpx;
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #004294 0%, #0066cc 100%);
|
||||
border-radius: 44rpx;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #cccccc;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,684 @@
|
|||
<template>
|
||||
<view class="service-records-page">
|
||||
<!-- 头部区域 -->
|
||||
<NavHeader title="服务记录" />
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="tab-bar">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'pending_payment' }"
|
||||
@click="switchTab('pending_payment')"
|
||||
>
|
||||
<text class="tab-text">待支付</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'pending_verification' }"
|
||||
@click="switchTab('pending_verification')"
|
||||
>
|
||||
<text class="tab-text">待核销</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'completed' }"
|
||||
@click="switchTab('completed')"
|
||||
>
|
||||
<text class="tab-text">已完成</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: currentTab === 'cancelled' }"
|
||||
@click="switchTab('cancelled')"
|
||||
>
|
||||
<text class="tab-text">已取消</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<scroll-view
|
||||
class="record-list"
|
||||
scroll-y="true"
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="handleRefresh"
|
||||
@scrolltolower="handleLoadMore"
|
||||
:lower-threshold="100"
|
||||
>
|
||||
<!-- 空数据提示 -->
|
||||
<view class="empty-state" v-if="!loading && currentList.length === 0">
|
||||
<image
|
||||
class="empty-icon"
|
||||
src="/static/home/entry_icon.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="empty-text">暂无{{ getTabLabel() }}记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 记录列表项 -->
|
||||
<view
|
||||
class="record-item"
|
||||
v-for="(item, index) in currentList"
|
||||
:key="index"
|
||||
@click="handleRecordClick(item)"
|
||||
>
|
||||
<view class="record-header">
|
||||
<view class="record-title-row">
|
||||
<text class="record-title">{{ item.serviceName }}</text>
|
||||
<view class="status-badge" :class="getStatusClass(item.status)">
|
||||
<text class="status-text">{{ getStatusText(item.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="record-time">{{ item.createTime }}</text>
|
||||
</view>
|
||||
|
||||
<view class="record-content">
|
||||
<view class="record-info-row">
|
||||
<text class="info-label">服务类型:</text>
|
||||
<text class="info-value">{{ item.serviceType }}</text>
|
||||
</view>
|
||||
<view class="record-info-row">
|
||||
<text class="info-label">服务门店:</text>
|
||||
<text class="info-value">{{ item.storeName }}</text>
|
||||
</view>
|
||||
<view class="record-info-row">
|
||||
<text class="info-label">订单号:</text>
|
||||
<text class="info-value">{{ item.orderNo }}</text>
|
||||
</view>
|
||||
<view class="record-info-row">
|
||||
<text class="info-label">订单金额:</text>
|
||||
<text class="info-value price">¥{{ item.amount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<view class="record-actions" v-if="item.status === 'pending_payment'">
|
||||
<button
|
||||
class="action-btn cancel-btn"
|
||||
@click.stop="handleCancel(item)"
|
||||
>
|
||||
取消订单
|
||||
</button>
|
||||
<button class="action-btn pay-btn" @click.stop="handlePay(item)">
|
||||
立即支付
|
||||
</button>
|
||||
</view>
|
||||
<view
|
||||
class="record-actions"
|
||||
v-else-if="item.status === 'pending_verification'"
|
||||
>
|
||||
<button
|
||||
class="action-btn detail-btn"
|
||||
@click.stop="handleViewDetail(item)"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
</view>
|
||||
<view class="record-actions" v-else-if="item.status === 'completed'">
|
||||
<button
|
||||
class="action-btn detail-btn"
|
||||
@click.stop="handleViewDetail(item)"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
<button
|
||||
class="action-btn review-btn"
|
||||
@click.stop="handleReview(item)"
|
||||
>
|
||||
评价
|
||||
</button>
|
||||
</view>
|
||||
<view class="record-actions" v-else-if="item.status === 'cancelled'">
|
||||
<button
|
||||
class="action-btn detail-btn"
|
||||
@click.stop="handleViewDetail(item)"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view class="load-more" v-if="currentList.length > 0">
|
||||
<text v-if="loadingMore" class="load-more-text">加载中...</text>
|
||||
<text v-else-if="!hasMore" class="load-more-text">没有更多数据了</text>
|
||||
<text v-else class="load-more-text">上拉加载更多</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavHeader from "@/components/NavHeader/NavHeader.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NavHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTab: "pending_payment", // 当前选中的 tab
|
||||
refreshing: false,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
// 假数据
|
||||
mockData: {
|
||||
pending_payment: [
|
||||
{
|
||||
id: 1,
|
||||
orderNo: "ORD20250101001",
|
||||
serviceName: "汽车维修保养",
|
||||
serviceType: "维修服务",
|
||||
storeName: "XX汽车维修中心",
|
||||
amount: "299.00",
|
||||
createTime: "2025-01-15 10:30:00",
|
||||
status: "pending_payment",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
orderNo: "ORD20250101002",
|
||||
serviceName: "家电清洗服务",
|
||||
serviceType: "清洗服务",
|
||||
storeName: "XX家电清洗店",
|
||||
amount: "158.00",
|
||||
createTime: "2025-01-14 15:20:00",
|
||||
status: "pending_payment",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
orderNo: "ORD20250101003",
|
||||
serviceName: "手机维修",
|
||||
serviceType: "维修服务",
|
||||
storeName: "XX手机维修店",
|
||||
amount: "199.00",
|
||||
createTime: "2025-01-13 09:15:00",
|
||||
status: "pending_payment",
|
||||
},
|
||||
],
|
||||
pending_verification: [
|
||||
{
|
||||
id: 4,
|
||||
orderNo: "ORD20250101004",
|
||||
serviceName: "汽车维修保养",
|
||||
serviceType: "维修服务",
|
||||
storeName: "XX汽车维修中心",
|
||||
amount: "299.00",
|
||||
createTime: "2025-01-10 14:30:00",
|
||||
status: "pending_verification",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
orderNo: "ORD20250101005",
|
||||
serviceName: "家电清洗服务",
|
||||
serviceType: "清洗服务",
|
||||
storeName: "XX家电清洗店",
|
||||
amount: "158.00",
|
||||
createTime: "2025-01-09 11:20:00",
|
||||
status: "pending_verification",
|
||||
},
|
||||
],
|
||||
completed: [
|
||||
{
|
||||
id: 6,
|
||||
orderNo: "ORD20250101006",
|
||||
serviceName: "汽车维修保养",
|
||||
serviceType: "维修服务",
|
||||
storeName: "XX汽车维修中心",
|
||||
amount: "299.00",
|
||||
createTime: "2025-01-05 16:30:00",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
orderNo: "ORD20250101007",
|
||||
serviceName: "家电清洗服务",
|
||||
serviceType: "清洗服务",
|
||||
storeName: "XX家电清洗店",
|
||||
amount: "158.00",
|
||||
createTime: "2025-01-04 10:20:00",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
orderNo: "ORD20250101008",
|
||||
serviceName: "手机维修",
|
||||
serviceType: "维修服务",
|
||||
storeName: "XX手机维修店",
|
||||
amount: "199.00",
|
||||
createTime: "2025-01-03 08:15:00",
|
||||
status: "completed",
|
||||
},
|
||||
],
|
||||
cancelled: [
|
||||
{
|
||||
id: 9,
|
||||
orderNo: "ORD20250101009",
|
||||
serviceName: "汽车维修保养",
|
||||
serviceType: "维修服务",
|
||||
storeName: "XX汽车维修中心",
|
||||
amount: "299.00",
|
||||
createTime: "2025-01-02 14:30:00",
|
||||
status: "cancelled",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
orderNo: "ORD20250101010",
|
||||
serviceName: "家电清洗服务",
|
||||
serviceType: "清洗服务",
|
||||
storeName: "XX家电清洗店",
|
||||
amount: "158.00",
|
||||
createTime: "2025-01-01 11:20:00",
|
||||
status: "cancelled",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentList() {
|
||||
return this.mockData[this.currentTab] || [];
|
||||
},
|
||||
},
|
||||
onLoad() {
|
||||
this.loadData();
|
||||
},
|
||||
methods: {
|
||||
// 切换 Tab
|
||||
switchTab(tab) {
|
||||
if (this.currentTab === tab) return;
|
||||
this.currentTab = tab;
|
||||
this.pageNo = 1;
|
||||
this.hasMore = true;
|
||||
this.loadData();
|
||||
},
|
||||
// 获取 Tab 标签文本
|
||||
getTabLabel() {
|
||||
const labels = {
|
||||
pending_payment: "待支付",
|
||||
pending_verification: "待核销",
|
||||
completed: "已完成",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
return labels[this.currentTab] || "";
|
||||
},
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
pending_payment: "待支付",
|
||||
pending_verification: "待核销",
|
||||
completed: "已完成",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
return statusMap[status] || "";
|
||||
},
|
||||
// 获取状态样式类
|
||||
getStatusClass(status) {
|
||||
const classMap = {
|
||||
pending_payment: "status-pending",
|
||||
pending_verification: "status-verification",
|
||||
completed: "status-completed",
|
||||
cancelled: "status-cancelled",
|
||||
};
|
||||
return classMap[status] || "";
|
||||
},
|
||||
// 加载数据
|
||||
loadData() {
|
||||
this.loading = true;
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
this.loading = false;
|
||||
this.refreshing = false;
|
||||
this.loadingMore = false;
|
||||
// 这里使用假数据,实际应该调用接口
|
||||
}, 500);
|
||||
},
|
||||
// 下拉刷新
|
||||
handleRefresh() {
|
||||
this.refreshing = true;
|
||||
this.pageNo = 1;
|
||||
this.hasMore = true;
|
||||
this.loadData();
|
||||
},
|
||||
// 上拉加载更多
|
||||
handleLoadMore() {
|
||||
if (this.hasMore && !this.loadingMore && !this.loading) {
|
||||
this.loadingMore = true;
|
||||
this.pageNo += 1;
|
||||
// 模拟加载更多
|
||||
setTimeout(() => {
|
||||
this.loadingMore = false;
|
||||
// 假数据没有更多了
|
||||
this.hasMore = false;
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
// 点击记录项
|
||||
handleRecordClick(item) {
|
||||
// 可以跳转到详情页
|
||||
console.log("点击记录:", item);
|
||||
},
|
||||
// 取消订单
|
||||
handleCancel(item) {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要取消该订单吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({
|
||||
title: "订单已取消",
|
||||
icon: "success",
|
||||
});
|
||||
// 这里应该调用接口取消订单,然后刷新列表
|
||||
// 暂时从待支付列表中移除
|
||||
const index = this.mockData.pending_payment.findIndex(
|
||||
(i) => i.id === item.id
|
||||
);
|
||||
if (index > -1) {
|
||||
this.mockData.pending_payment.splice(index, 1);
|
||||
// 添加到已取消列表
|
||||
this.mockData.cancelled.unshift({
|
||||
...item,
|
||||
status: "cancelled",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
// 立即支付
|
||||
handlePay(item) {
|
||||
uni.showToast({
|
||||
title: "跳转支付页面",
|
||||
icon: "none",
|
||||
});
|
||||
// 这里应该跳转到支付页面
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/payment/payment?orderNo=${item.orderNo}&amount=${item.amount}`
|
||||
// });
|
||||
},
|
||||
// 查看详情
|
||||
handleViewDetail(item) {
|
||||
uni.showToast({
|
||||
title: "查看详情",
|
||||
icon: "none",
|
||||
});
|
||||
// 这里应该跳转到详情页面
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/order/detail?orderNo=${item.orderNo}`
|
||||
// });
|
||||
},
|
||||
// 评价
|
||||
handleReview(item) {
|
||||
uni.showToast({
|
||||
title: "跳转评价页面",
|
||||
icon: "none",
|
||||
});
|
||||
// 这里应该跳转到评价页面
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/order/review?orderNo=${item.orderNo}`
|
||||
// });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.service-records-page {
|
||||
min-height: 100vh;
|
||||
background: #e2e8f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Tab 切换栏 */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
padding: 0 20rpx;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 88rpx;
|
||||
position: relative;
|
||||
|
||||
.tab-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.tab-text {
|
||||
color: #004294;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60rpx;
|
||||
height: 4rpx;
|
||||
background-color: #004294;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表区域 */
|
||||
.record-list {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
height: 0; // 配合 flex: 1 使用
|
||||
box-sizing: border-box;
|
||||
|
||||
/* 空数据提示 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
min-height: 500rpx;
|
||||
|
||||
.empty-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 40rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载更多提示 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
min-height: 80rpx;
|
||||
|
||||
.load-more-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 记录项 */
|
||||
.record-item {
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.record-header {
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.record-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.record-title {
|
||||
flex: 1;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
|
||||
.status-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background-color: rgba(255, 107, 0, 0.1);
|
||||
.status-text {
|
||||
color: #ff6b00;
|
||||
}
|
||||
}
|
||||
|
||||
&.status-verification {
|
||||
background-color: rgba(0, 66, 148, 0.1);
|
||||
.status-text {
|
||||
color: #004294;
|
||||
}
|
||||
}
|
||||
|
||||
&.status-completed {
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
.status-text {
|
||||
color: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
&.status-cancelled {
|
||||
background-color: rgba(158, 158, 158, 0.1);
|
||||
.status-text {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 400;
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.record-content {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.record-info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16rpx;
|
||||
line-height: 1.5;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #888888;
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
flex: 1;
|
||||
|
||||
&.price {
|
||||
color: #d51c3c;
|
||||
font-weight: 600;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
button{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 131rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 10rpx;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.cancel-btn {
|
||||
background-color: #f5f5f5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
&.pay-btn {
|
||||
background-color: #004294;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.detail-btn {
|
||||
background-color: #f5f5f5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
&.review-btn {
|
||||
background-color: #004294;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,861 @@
|
|||
<template>
|
||||
<view class="service-page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="header-content">
|
||||
<view class="search-box">
|
||||
<image
|
||||
class="search-icon"
|
||||
src="/static/service/search_icon.png"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="请输入搜索内容"
|
||||
v-model="searchKeyword"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
<view class="search-btn" @click="handleSearch">
|
||||
<text>搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类标签栏 -->
|
||||
<view class="category-bar">
|
||||
<view class="location-selector" @click="handleLocationSelect">
|
||||
<text class="location-text">{{ selectedDistance ? `附近${selectedDistance}km` : '附近' }}</text>
|
||||
<image
|
||||
class="location-arrow"
|
||||
src="/static/service/location-arrow.png"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
<scroll-view
|
||||
class="category-scroll"
|
||||
scroll-x="true"
|
||||
:show-scrollbar="false"
|
||||
:scroll-left="scrollLeft"
|
||||
:scroll-with-animation="true"
|
||||
enable-passive
|
||||
>
|
||||
<view class="category-list">
|
||||
<view
|
||||
class="category-item"
|
||||
:class="{ active: currentCategory === item.value }"
|
||||
v-for="(item, index) in categoryList"
|
||||
:key="item.id"
|
||||
@click="handleCategoryClick(item, index)"
|
||||
>
|
||||
<text>{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 服务列表 -->
|
||||
<scroll-view
|
||||
class="service-list"
|
||||
scroll-y="true"
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="handleRefresh"
|
||||
@scrolltolower="handleLoadMore"
|
||||
:lower-threshold="100"
|
||||
>
|
||||
<!-- 空数据提示 -->
|
||||
<view class="empty-state" v-if="!loading && serviceList.length === 0">
|
||||
<image
|
||||
class="empty-icon"
|
||||
src="/static/home/entry_icon.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="empty-text">暂无服务数据</text>
|
||||
<text class="empty-desc">请尝试切换分类或搜索条件</text>
|
||||
</view>
|
||||
|
||||
<!-- 服务列表项 -->
|
||||
<view
|
||||
class="service-item"
|
||||
v-for="(item, index) in serviceList"
|
||||
:key="index"
|
||||
@click="handleServiceItemClick(item)"
|
||||
>
|
||||
<!-- 左侧图片 -->
|
||||
<view class="service-image-wrapper">
|
||||
<image
|
||||
class="service-image"
|
||||
:src="item.coverUrl"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<!-- 右侧信息 -->
|
||||
<view class="service-info">
|
||||
<view class="service-title-row">
|
||||
<text class="service-title">{{ item.name }}</text>
|
||||
<view class="distance-info">
|
||||
<image
|
||||
class="location-icon"
|
||||
src="/static/service/location-icon.png"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<text class="distance-text">{{ item.distance || 0 }}km</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="service-detail">
|
||||
<text class="detail-label">门店地址:</text>
|
||||
<text class="detail-value">{{ item.address }}</text>
|
||||
</view>
|
||||
|
||||
<view class="service-detail">
|
||||
<text class="detail-label">联系电话:</text>
|
||||
<text class="detail-value">{{ item.phone }}</text>
|
||||
</view>
|
||||
|
||||
<view class="service-tags-row">
|
||||
<view class="service-tags">
|
||||
<view class="tag-item tag-pink"> 会员特惠 </view>
|
||||
<view class="tag-item tag-orange">{{
|
||||
currentCategoryLabel
|
||||
}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="enter-btn" @click.stop="handleEnterStore(item)">
|
||||
进店
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view class="load-more" v-if="serviceList.length > 0">
|
||||
<text v-if="loadingMore" class="load-more-text">加载中...</text>
|
||||
<text v-else-if="!hasMore" class="load-more-text">没有更多数据了</text>
|
||||
<text v-else class="load-more-text">上拉加载更多</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 距离选择弹窗 -->
|
||||
<view class="distance-picker-mask" v-if="showDistancePicker" @click="showDistancePicker = false">
|
||||
<view class="distance-picker" @click.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-title">选择距离范围</text>
|
||||
<view class="picker-close" @click="showDistancePicker = false">×</view>
|
||||
</view>
|
||||
<view class="picker-options">
|
||||
<view
|
||||
class="picker-option"
|
||||
:class="{ active: selectedDistance === null }"
|
||||
@click="clearDistance"
|
||||
>
|
||||
<text>不限距离</text>
|
||||
</view>
|
||||
<view
|
||||
class="picker-option"
|
||||
:class="{ active: selectedDistance === 1 }"
|
||||
@click="selectDistance(1)"
|
||||
>
|
||||
<text>1km</text>
|
||||
</view>
|
||||
<view
|
||||
class="picker-option"
|
||||
:class="{ active: selectedDistance === 3 }"
|
||||
@click="selectDistance(3)"
|
||||
>
|
||||
<text>3km</text>
|
||||
</view>
|
||||
<view
|
||||
class="picker-option"
|
||||
:class="{ active: selectedDistance === 5 }"
|
||||
@click="selectDistance(5)"
|
||||
>
|
||||
<text>5km</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDictDataByType, getGuildStorePage } from "@/api/service";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 0,
|
||||
searchKeyword: "",
|
||||
currentCategory: null, // 默认选中"维修"
|
||||
currentCategoryLabel: "",
|
||||
scrollLeft: 0,
|
||||
categoryList: [],
|
||||
serviceList: [],
|
||||
// 分页相关
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
hasMore: true,
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
refreshing: false,
|
||||
// 位置相关
|
||||
selectedDistance: null, // 选中的距离(km),null表示不限距离
|
||||
showDistancePicker: false, // 是否显示距离选择器
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.getSystemInfo();
|
||||
this.getServiceCategoryFun(); // 获取服务分类
|
||||
this.checkAndRequestLocation(); // 检查并请求位置权限
|
||||
},
|
||||
onShow() {
|
||||
// 如果分类列表已经加载,检查是否有需要高亮的分类
|
||||
if (this.categoryList && this.categoryList.length > 0) {
|
||||
this.checkAndSetCategory();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取系统信息
|
||||
getSystemInfo() {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || 0;
|
||||
},
|
||||
// 获取服务分类
|
||||
getServiceCategoryFun() {
|
||||
getDictDataByType({ type: "member_labor_service_type" }).then((res) => {
|
||||
if (res) {
|
||||
this.categoryList = res || [];
|
||||
// 分类列表加载完成后,检查是否有需要高亮的分类
|
||||
this.checkAndSetCategory();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 检查并设置需要高亮的分类
|
||||
checkAndSetCategory() {
|
||||
const app = getApp();
|
||||
if (app && app.globalData && app.globalData.serviceCategory) {
|
||||
const categoryValue = app.globalData.serviceCategory;
|
||||
// 清除 globalData 中的分类信息
|
||||
app.globalData.serviceCategory = null;
|
||||
|
||||
// 在分类列表中查找匹配的分类(通过 value 匹配)
|
||||
const matchedCategory = this.categoryList.find(
|
||||
item => item.value === categoryValue
|
||||
);
|
||||
|
||||
if (matchedCategory) {
|
||||
// 设置当前分类并高亮
|
||||
this.currentCategory = matchedCategory.value;
|
||||
this.currentCategoryLabel = matchedCategory.label;
|
||||
// 重置分页,重新加载数据
|
||||
this.pageNo = 1;
|
||||
this.serviceList = [];
|
||||
this.hasMore = true;
|
||||
this.loadServiceList();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有从 globalData 获取到分类,且当前没有选中分类,默认高亮第一个分类
|
||||
if (this.currentCategory === null && this.categoryList && this.categoryList.length > 0) {
|
||||
const firstCategory = this.categoryList[0];
|
||||
this.currentCategory = firstCategory.value;
|
||||
this.currentCategoryLabel = firstCategory.label;
|
||||
// 重置分页,重新加载数据
|
||||
this.pageNo = 1;
|
||||
this.serviceList = [];
|
||||
this.hasMore = true;
|
||||
this.loadServiceList();
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索
|
||||
handleSearch() {
|
||||
// 重置分页,重新加载
|
||||
this.pageNo = 1;
|
||||
this.serviceList = [];
|
||||
this.hasMore = true;
|
||||
this.loadServiceList();
|
||||
},
|
||||
|
||||
// 检查并请求位置权限
|
||||
checkAndRequestLocation() {
|
||||
// 先检查是否已有存储的位置信息
|
||||
const savedLocation = uni.getStorageSync("userLocation");
|
||||
if (savedLocation && savedLocation.latitude && savedLocation.longitude) {
|
||||
// 已有位置信息,不需要再次获取
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有位置信息,请求授权并获取位置
|
||||
uni.authorize({
|
||||
scope: "scope.userLocation",
|
||||
success: () => {
|
||||
// 授权成功,获取位置
|
||||
this.getUserLocation();
|
||||
},
|
||||
fail: () => {
|
||||
// 授权失败,提示用户
|
||||
uni.showModal({
|
||||
title: "位置权限",
|
||||
content: "需要获取您的位置信息以提供附近店铺服务,是否前往设置开启?",
|
||||
confirmText: "去设置",
|
||||
cancelText: "取消",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.openSetting({
|
||||
success: (settingRes) => {
|
||||
if (settingRes.authSetting["scope.userLocation"]) {
|
||||
// 用户开启了位置权限,获取位置
|
||||
this.getUserLocation();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 获取用户当前位置
|
||||
getUserLocation() {
|
||||
uni.getLocation({
|
||||
type: "gcj02",
|
||||
success: (res) => {
|
||||
const location = {
|
||||
latitude: res.latitude,
|
||||
longitude: res.longitude,
|
||||
};
|
||||
// 存储位置信息
|
||||
uni.setStorageSync("userLocation", location);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error("获取位置失败:", err);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 选择位置/距离
|
||||
handleLocationSelect() {
|
||||
this.showDistancePicker = true;
|
||||
},
|
||||
|
||||
// 选择距离
|
||||
selectDistance(distance) {
|
||||
this.selectedDistance = distance;
|
||||
this.showDistancePicker = false;
|
||||
// 重置分页,重新加载数据
|
||||
this.pageNo = 1;
|
||||
this.serviceList = [];
|
||||
this.hasMore = true;
|
||||
this.loadServiceList();
|
||||
},
|
||||
|
||||
// 清除距离筛选
|
||||
clearDistance() {
|
||||
this.selectedDistance = null;
|
||||
this.showDistancePicker = false;
|
||||
// 重置分页,重新加载数据
|
||||
this.pageNo = 1;
|
||||
this.serviceList = [];
|
||||
this.hasMore = true;
|
||||
this.loadServiceList();
|
||||
},
|
||||
|
||||
// 分类点击
|
||||
handleCategoryClick(item) {
|
||||
this.currentCategory = item.value;
|
||||
this.currentCategoryLabel = item.label;
|
||||
// 重置分页,重新加载
|
||||
this.pageNo = 1;
|
||||
this.serviceList = [];
|
||||
this.hasMore = true;
|
||||
this.loadServiceList();
|
||||
},
|
||||
|
||||
// 进店按钮点击
|
||||
handleServiceItemClick(item) {
|
||||
// 跳转到店铺详情页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/serviceDetail?id=${item.id}&categoryLabel=${this.currentCategoryLabel}`,
|
||||
});
|
||||
},
|
||||
|
||||
// 进店按钮点击 - 跳转到地图详情页
|
||||
handleEnterStore(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/detail/mapDetail?id=${item.id}`,
|
||||
});
|
||||
},
|
||||
|
||||
// 加载服务列表
|
||||
async loadServiceList(isLoadMore = false) {
|
||||
// 如果正在加载或没有更多数据,则不加载
|
||||
if (this.loading || this.loadingMore || (!isLoadMore && !this.hasMore)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isLoadMore) {
|
||||
this.loadingMore = true;
|
||||
} else {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
// 构建请求参数
|
||||
const params = {
|
||||
pageNo: this.pageNo,
|
||||
pageSize: this.pageSize,
|
||||
type: this.currentCategory,
|
||||
name: this.searchKeyword,
|
||||
};
|
||||
|
||||
// 如果选择了距离,添加 distance 参数
|
||||
if (this.selectedDistance !== null) {
|
||||
params.distance = this.selectedDistance;
|
||||
}
|
||||
|
||||
const res = await getGuildStorePage(params);
|
||||
|
||||
if (res) {
|
||||
const newList = res.list || [];
|
||||
this.total = res.total || 0;
|
||||
|
||||
if (isLoadMore) {
|
||||
// 加载更多,追加数据
|
||||
this.serviceList = [...this.serviceList, ...newList];
|
||||
} else {
|
||||
// 首次加载或刷新,替换数据
|
||||
this.serviceList = newList;
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
this.hasMore = this.serviceList.length < this.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载服务列表失败:", error);
|
||||
uni.showToast({
|
||||
title: "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.loadingMore = false;
|
||||
this.refreshing = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
handleRefresh() {
|
||||
this.refreshing = true;
|
||||
this.pageNo = 1;
|
||||
this.hasMore = true;
|
||||
this.loadServiceList(false);
|
||||
},
|
||||
|
||||
// 上拉加载更多
|
||||
handleLoadMore() {
|
||||
if (this.hasMore && !this.loadingMore && !this.loading) {
|
||||
this.pageNo += 1;
|
||||
this.loadServiceList(true);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.service-page {
|
||||
height: 100vh;
|
||||
background-color: #e2e8f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.header {
|
||||
margin-left: 22rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 488rpx;
|
||||
height: 60rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 58rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 13rpx 9rpx 13rpx 18rpx;
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-icon {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
height: 100%;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #888888;
|
||||
}
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
width: 120rpx;
|
||||
height: 47rpx;
|
||||
background: #004294;
|
||||
border-radius: 24rpx 24rpx 24rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 分类标签栏 */
|
||||
.category-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
|
||||
.location-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx 0 46rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
.location-text {
|
||||
margin-right: 8rpx;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 26rpx;
|
||||
color: #494949;
|
||||
}
|
||||
|
||||
.location-arrow {
|
||||
width: 18rpx;
|
||||
height: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.category-scroll {
|
||||
flex: 1;
|
||||
width: 0; // 重要:flex布局时需要设置width: 0才能正确计算宽度
|
||||
height: 60rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.category-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
padding-right: 30rpx; // 最后一个元素右边距
|
||||
|
||||
.category-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 33rpx;
|
||||
height: 60rpx;
|
||||
position: relative;
|
||||
flex-shrink: 0; // 重要:防止被压缩
|
||||
white-space: nowrap;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.active {
|
||||
text {
|
||||
color: #004294;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// &::after {
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// bottom: 0;
|
||||
// left: 24rpx;
|
||||
// right: 24rpx;
|
||||
// height: 4rpx;
|
||||
// background-color: #004294;
|
||||
// border-radius: 2rpx;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 服务列表 */
|
||||
.service-list {
|
||||
width: 95%;
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
height: 0; // 配合 flex: 1 使用,让 scroll-view 可以滚动
|
||||
|
||||
/* 空数据提示 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 200rpx 0;
|
||||
min-height: 500rpx;
|
||||
|
||||
.empty-icon {
|
||||
width: 356rpx;
|
||||
height: 266rpx;
|
||||
margin-bottom: 40rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
color: #cccccc;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载更多提示 */
|
||||
.load-more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
min-height: 80rpx;
|
||||
|
||||
.load-more-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 21rpx;
|
||||
padding: 25rpx 30rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
|
||||
.service-image-wrapper {
|
||||
width: 186rpx;
|
||||
height: 200rpx;
|
||||
margin-right: 24rpx;
|
||||
|
||||
.service-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.service-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.service-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.service-title {
|
||||
flex: 1;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
|
||||
.distance-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.location-icon {
|
||||
width: 17rpx;
|
||||
height: 20rpx;
|
||||
margin-right: 5rpx;
|
||||
}
|
||||
|
||||
.distance-text {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 22rpx;
|
||||
color: #004294;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.service-detail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.5;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
color: #888888;
|
||||
|
||||
.detail-label {
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.service-tags-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
|
||||
.service-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex: 1;
|
||||
|
||||
.tag-item {
|
||||
padding: 7rpx 12rpx;
|
||||
font-size: 20rpx;
|
||||
|
||||
&.tag-pink {
|
||||
background-color: rgba(213, 28, 60, 0.1);
|
||||
color: #d51c3c;
|
||||
}
|
||||
|
||||
&.tag-orange {
|
||||
background-color: rgba(255, 107, 0, 0.1);
|
||||
color: #ff6b00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enter-btn {
|
||||
position: absolute;
|
||||
right: 21rpx;
|
||||
bottom: 25rpx;
|
||||
padding: 12rpx 40rpx;
|
||||
background: #004294;
|
||||
border-radius: 25rpx 25rpx 25rpx 25rpx;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 26rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 距离选择器 */
|
||||
.distance-picker-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.distance-picker {
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
padding: 0 0 40rpx 0;
|
||||
max-height: 60vh;
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 30rpx 20rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.picker-title {
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
color: #1a1819;
|
||||
}
|
||||
|
||||
.picker-close {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48rpx;
|
||||
color: #999999;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-options {
|
||||
padding: 20rpx 0;
|
||||
|
||||
.picker-option {
|
||||
padding: 30rpx;
|
||||
font-family: PingFang-SC, PingFang-SC;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #004294;
|
||||
background-color: rgba(0, 66, 148, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1018 B |
|
After Width: | Height: | Size: 524 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
uni.addInterceptor({
|
||||
returnValue (res) {
|
||||
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||
return res;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
res.then((res) => {
|
||||
if (!res) return resolve(res)
|
||||
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* 时间戳转换工具
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化时间戳
|
||||
* @param {Number|String} timestamp 时间戳(秒或毫秒)
|
||||
* @param {String} format 格式化模板,默认 'YYYY.MM.DD HH:mm'
|
||||
* @returns {String} 格式化后的时间字符串
|
||||
*/
|
||||
export function formatTime(timestamp, format = 'YYYY.MM.DD HH:mm') {
|
||||
if (!timestamp) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 将时间戳转换为数字
|
||||
let ts = Number(timestamp);
|
||||
|
||||
// 判断是秒级还是毫秒级时间戳(大于10位数是毫秒级)
|
||||
if (ts.toString().length === 10) {
|
||||
ts = ts * 1000;
|
||||
}
|
||||
|
||||
const date = new Date(ts);
|
||||
|
||||
// 检查日期是否有效
|
||||
if (isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间戳为默认格式 YYYY.MM.DD HH:mm
|
||||
* @param {Number|String} timestamp 时间戳
|
||||
* @returns {String} 格式化后的时间字符串
|
||||
*/
|
||||
export function formatDateTime(timestamp) {
|
||||
return formatTime(timestamp, 'YYYY.MM.DD HH:mm');
|
||||
}
|
||||
|
||||