Initial commit

cz_dev
wk 2025-12-19 20:27:55 +08:00
commit 33a32482d0
59 changed files with 7689 additions and 0 deletions

42
.gitignore vendored 100644
View File

@ -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

52
App.vue 100644
View File

@ -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: () => {
// switchTabtabBar使reLaunch
uni.reLaunch({
url: '/pages/index/index'
})
}
})
}
}, 100)
}
}
}
}
</script>
<style>
/*每个页面公共css */
</style>

55
api/auth.js 100644
View File

@ -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认证
})
}

102
api/home.js 100644
View File

@ -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
})
}

319
api/index.js 100644
View File

@ -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)
}
})
})
}

62
api/profile.js 100644
View File

@ -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认证
})
}

40
api/service.js 100644
View File

@ -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,
})
}

View File

@ -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>

20
index.html 100644
View File

@ -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>

22
main.js 100644
View File

@ -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

75
manifest.json 100644
View File

@ -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"
}

141
pages.json 100644
View File

@ -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": {}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
// refreshTokenaccessToken
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
})
})
//
//
// codeencryptedDataiv
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)
}
// refreshTokenaccessToken
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>

View File

@ -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();
//
// iOS44pxiPhone X20pxiPhone 8
// Android24px
let defaultHeight = 20;
// #ifdef APP-PLUS
if (systemInfo.platform === "ios") {
// iPhone X44px
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>

View File

@ -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>

View File

@ -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>

View File

@ -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, // kmnull
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; // flexwidth: 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
static/logo.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -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])
});
});
},
});

76
uni.scss 100644
View File

@ -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;

55
utils/date.js 100644
View File

@ -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');
}