Browse Source

Initial commit

pull/1/head
SwTt29 3 months ago
commit
7096db84e6
  1. 1
      .gitignore
  2. 108
      app.js
  3. 76
      app.json
  4. 44
      app.wxss
  5. 102
      components/navigation-bar/navigation-bar.js
  6. 5
      components/navigation-bar/navigation-bar.json
  7. 64
      components/navigation-bar/navigation-bar.wxml
  8. 96
      components/navigation-bar/navigation-bar.wxss
  9. 348
      custom-tab-bar/index.js
  10. 4
      custom-tab-bar/index.json
  11. 47
      custom-tab-bar/index.wxml
  12. 176
      custom-tab-bar/index.wxss
  13. 13
      images/logo.svg
  14. BIN
      images/生成鸡蛋贸易平台图片.png
  15. 9370
      miniprogram_npm/accepts/miniprogram_npm/mime-db/index.js
  16. 1
      miniprogram_npm/accepts/miniprogram_npm/mime-db/index.js.map
  17. 1171
      package-lock.json
  18. 21
      package.json
  19. 1945
      pages/buyer/index.js
  20. 5
      pages/buyer/index.json
  21. 296
      pages/buyer/index.wxml
  22. 464
      pages/buyer/index.wxss
  23. 66
      pages/debug/debug-gross-weight.js
  24. 2
      pages/debug/debug-gross-weight.wxml
  25. 66
      pages/debug/debug-sold-out.js
  26. 2
      pages/debug/debug-sold-out.wxml
  27. 66
      pages/debug/debug.js
  28. 2
      pages/debug/debug.wxml
  29. 557
      pages/evaluate/index.js
  30. 6
      pages/evaluate/index.json
  31. 339
      pages/evaluate/index.wxml
  32. 769
      pages/evaluate/index.wxss
  33. 977
      pages/index/index.js
  34. 5
      pages/index/index.json
  35. 94
      pages/index/index.wxml
  36. 243
      pages/index/index.wxss
  37. 64
      pages/notopen/index.js
  38. 4
      pages/notopen/index.json
  39. 8
      pages/notopen/index.wxml
  40. 69
      pages/notopen/index.wxss
  41. 652
      pages/profile/index.js
  42. 3
      pages/profile/index.json
  43. 63
      pages/profile/index.wxml
  44. 1
      pages/profile/index.wxss
  45. 588
      pages/publish/index.js
  46. 3
      pages/publish/index.json
  47. 54
      pages/publish/index.wxml
  48. 117
      pages/publish/index.wxss
  49. 3333
      pages/seller/index.js
  50. 5
      pages/seller/index.json
  51. 833
      pages/seller/index.wxml
  52. 295
      pages/seller/index.wxss
  53. 1164
      pages/settlement/index.js
  54. 4
      pages/settlement/index.json
  55. 367
      pages/settlement/index.wxml
  56. 1697
      pages/settlement/index.wxss
  57. 66
      pages/test-tools/api-test.js
  58. 2
      pages/test-tools/api-test.wxml
  59. 66
      pages/test-tools/clear-storage.js
  60. 2
      pages/test-tools/clear-storage.wxml
  61. 66
      pages/test-tools/connection-test.js
  62. 2
      pages/test-tools/connection-test.wxml
  63. 66
      pages/test-tools/fix-connection.js
  64. 2
      pages/test-tools/fix-connection.wxml
  65. 66
      pages/test-tools/gross-weight-tester.js
  66. 2
      pages/test-tools/gross-weight-tester.wxml
  67. 66
      pages/test-tools/phone-test.js
  68. 2
      pages/test-tools/phone-test.wxml
  69. 66
      pages/test-tools/test-mode-switch.js
  70. 2
      pages/test-tools/test-mode-switch.wxml
  71. 66
      pages/test/undercarriage-test.js
  72. 2
      pages/test/undercarriage-test.wxml
  73. 91
      project.config.json
  74. 23
      project.private.config.json
  75. 24
      server-example/.env
  76. 101
      server-example/.env.example.mysql
  77. 41
      server-example/add-department-column.js
  78. 143
      server-example/complete-gross-weight-fix.js
  79. 123
      server-example/complete-gross-weight-verification.js
  80. 155
      server-example/create-missing-associations.js
  81. 356
      server-example/database-extension.js
  82. 175
      server-example/direct-db-check.js
  83. 86
      server-example/ecosystem.config.js
  84. 61
      server-example/find-product-creator.js
  85. 85
      server-example/fixed-server.js
  86. 87
      server-example/free-port.js
  87. 5
      server-example/gross-weight-fix-error.json
  88. 24
      server-example/gross-weight-frontend-fix-report.json
  89. 135
      server-example/gross-weight-log-analyzer.js
  90. 67
      server-example/list-users.js
  91. 86
      server-example/logger.js
  92. 8
      server-example/oss-config.js
  93. 319
      server-example/oss-uploader.js
  94. 2393
      server-example/package-lock.json
  95. 35
      server-example/package.json
  96. 236
      server-example/port-conflict-fix.js
  97. 71
      server-example/query-database.js
  98. 2973
      server-example/server-mysql-backup-alias.js
  99. 2973
      server-example/server-mysql-backup-count.js
  100. 2973
      server-example/server-mysql-backup-final.js

1
.gitignore

@ -0,0 +1 @@
node_modules

108
app.js

@ -0,0 +1,108 @@
App({
onLaunch: function () {
// 初始化应用
console.log('App Launch')
// 初始化本地存储的标签和用户数据
if (!wx.getStorageSync('users')) {
wx.setStorageSync('users', {})
}
if (!wx.getStorageSync('tags')) {
wx.setStorageSync('tags', {})
}
if (!wx.getStorageSync('goods')) {
// 初始化空的商品列表,不预置默认数据,由服务器获取
wx.setStorageSync('goods', [])
}
if (!wx.getStorageSync('supplies')) {
// 初始化空的供应列表,不预置默认数据,由服务器获取
wx.setStorageSync('supplies', [])
}
// 检查是否是首次启动
const isFirstLaunch = !wx.getStorageSync('hasLaunched')
if (isFirstLaunch) {
// 标记应用已经启动过
wx.setStorageSync('hasLaunched', true)
// 只有在首次启动时才检查用户身份并可能跳转
const userId = wx.getStorageSync('userId')
if (userId) {
const users = wx.getStorageSync('users')
const user = users[userId]
if (user && user.type) {
// 延迟跳转,确保页面加载完成
setTimeout(() => {
try {
if (user.type === 'buyer') {
wx.switchTab({ url: '/pages/buyer/index' })
} else if (user.type === 'seller') {
wx.switchTab({ url: '/pages/seller/index' })
}
} catch (e) {
console.error('启动时页面跳转异常:', e)
// 即使跳转失败,也不影响应用正常启动
}
}, 100)
}
}
}
// 获取用户信息
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo 获取头像昵称
wx.getUserInfo({
success: res => {
this.globalData.userInfo = res.userInfo
// 存储用户ID(实际项目中使用openid)
if (!wx.getStorageSync('userId')) {
const userId = 'user_' + Date.now()
wx.setStorageSync('userId', userId)
// 初始化用户数据
const users = wx.getStorageSync('users')
users[userId] = {
info: res.userInfo,
type: null
}
wx.setStorageSync('users', users)
}
}
})
}
}
})
},
onShow: function () {
console.log('App Show')
},
onHide: function () {
console.log('App Hide')
},
// 更新当前选中的tab
updateCurrentTab(tabKey) {
if (this.globalData) {
this.globalData.currentTab = tabKey
}
},
// 跳转到估价页面
goToEvaluatePage() {
wx.navigateTo({
url: '/pages/evaluate/index'
})
},
// 上传手机号数据
async uploadPhoneNumberData(phoneData) {
const API = require('./utils/api.js')
return await API.uploadPhoneNumberData(phoneData)
},
globalData: {
userInfo: null,
currentTab: 'index' // 当前选中的tab
}
})

76
app.json

@ -0,0 +1,76 @@
{
"pages": [
"pages/index/index",
"pages/evaluate/index",
"pages/settlement/index",
"pages/publish/index",
"pages/buyer/index",
"pages/seller/index",
"pages/profile/index",
"pages/notopen/index"
],
"subpackages": [
{
"root": "pages/debug",
"pages": [
"debug",
"debug-sold-out",
"debug-gross-weight"
],
"independent": false
},
{
"root": "pages/test",
"pages": [
"undercarriage-test"
],
"independent": false
},
{
"root": "pages/test-tools",
"pages": [
"test-mode-switch",
"connection-test",
"api-test",
"phone-test",
"clear-storage",
"gross-weight-tester",
"fix-connection"
],
"independent": false
}
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "又鸟蛋平台",
"navigationBarTextStyle": "black"
},
"tabBar": {
"custom": true,
"color": "#999999",
"selectedColor": "#FF6B81",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/buyer/index",
"text": "购物"
},
{
"pagePath": "pages/seller/index",
"text": "货源"
},
{
"pagePath": "pages/profile/index",
"text": "我的"
}
]
},
"style": "v2",
"sitemapLocation": "sitemap.json"
}

44
app.wxss

@ -0,0 +1,44 @@
/**app.wxss**/
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background-color: #f5f5f5;
}
.btn {
width: 80%;
padding: 15rpx 0;
margin: 20rpx 0;
border-radius: 10rpx;
font-size: 32rpx;
}
.card {
width: 90%;
padding: 30rpx;
margin: 20rpx 0;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
}
.title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 20rpx;
}
.input {
width: 100%;
max-width: 100%;
padding: 20rpx;
margin: 15rpx 0;
border: 1px solid #eee;
border-radius: 5rpx;
font-size: 28rpx;
box-sizing: border-box;
}

102
components/navigation-bar/navigation-bar.js

@ -0,0 +1,102 @@
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
/**
* 组件的属性列表
*/
properties: {
extClass: {
type: String,
value: ''
},
title: {
type: String,
value: ''
},
background: {
type: String,
value: ''
},
color: {
type: String,
value: ''
},
back: {
type: Boolean,
value: true
},
loading: {
type: Boolean,
value: false
},
homeButton: {
type: Boolean,
value: false,
},
animated: {
// 显示隐藏的时候opacity动画效果
type: Boolean,
value: true
},
show: {
// 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在
type: Boolean,
value: true,
observer: '_showChange'
},
// back为true的时候,返回的页面深度
delta: {
type: Number,
value: 1
},
},
/**
* 组件的初始数据
*/
data: {
displayStyle: ''
},
lifetimes: {
attached() {
const rect = wx.getMenuButtonBoundingClientRect()
const platform = (wx.getDeviceInfo() || wx.getSystemInfoSync()).platform
const isAndroid = platform === 'android'
const isDevtools = platform === 'devtools'
const { windowWidth, safeArea: { top = 0, bottom = 0 } = {} } = wx.getWindowInfo() || wx.getSystemInfoSync()
this.setData({
ios: !isAndroid,
innerPaddingRight: `padding-right: ${windowWidth - rect.left}px`,
leftWidth: `width: ${windowWidth - rect.left}px`,
safeAreaTop: isDevtools || isAndroid ? `height: calc(var(--height) + ${top}px); padding-top: ${top}px` : ``
})
},
},
/**
* 组件的方法列表
*/
methods: {
_showChange(show) {
const animated = this.data.animated
let displayStyle = ''
if (animated) {
displayStyle = `opacity: ${show ? '1' : '0'
};transition:opacity 0.5s;`
} else {
displayStyle = `display: ${show ? '' : 'none'}`
}
this.setData({
displayStyle
})
},
back() {
const data = this.data
if (data.delta) {
wx.navigateBack({
delta: data.delta
})
}
this.triggerEvent('back', { delta: data.delta }, {})
}
},
})

5
components/navigation-bar/navigation-bar.json

@ -0,0 +1,5 @@
{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {}
}

64
components/navigation-bar/navigation-bar.wxml

@ -0,0 +1,64 @@
<view class="weui-navigation-bar {{extClass}}">
<view class="weui-navigation-bar__inner {{ios ? 'ios' : 'android'}}" style="color: {{color}}; background: {{background}}; {{displayStyle}}; {{innerPaddingRight}}; {{safeAreaTop}};">
<!-- 左侧按钮 -->
<view class='weui-navigation-bar__left' style="{{leftWidth}};">
<block wx:if="{{back || homeButton}}">
<!-- 返回上一页 -->
<block wx:if="{{back}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_goback">
<view
bindtap="back"
class="weui-navigation-bar__btn_goback_wrapper"
hover-class="weui-active"
hover-stay-time="100"
aria-role="button"
aria-label="返回"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_goback"></view>
</view>
</view>
</block>
<!-- 返回首页 -->
<block wx:if="{{homeButton}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_home">
<view
bindtap="home"
class="weui-navigation-bar__btn_home_wrapper"
hover-class="weui-active"
aria-role="button"
aria-label="首页"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_home"></view>
</view>
</view>
</block>
</block>
<block wx:else>
<slot name="left"></slot>
</block>
</view>
<!-- 标题 -->
<view class='weui-navigation-bar__center'>
<view wx:if="{{loading}}" class="weui-navigation-bar__loading" aria-role="alert">
<view
class="weui-loading"
aria-role="img"
aria-label="加载中"
></view>
</view>
<block wx:if="{{title}}">
<text>{{title}}</text>
</block>
<block wx:else>
<slot name="center"></slot>
</block>
</view>
<!-- 右侧留空 -->
<view class='weui-navigation-bar__right'>
<slot name="right"></slot>
</view>
</view>
</view>

96
components/navigation-bar/navigation-bar.wxss

@ -0,0 +1,96 @@
.weui-navigation-bar {
--weui-FG-0:rgba(0,0,0,.9);
--height: 44px;
--left: 16px;
}
.weui-navigation-bar .android {
--height: 48px;
}
.weui-navigation-bar {
overflow: hidden;
color: var(--weui-FG-0);
flex: none;
}
.weui-navigation-bar__inner {
position: relative;
top: 0;
left: 0;
height: calc(var(--height) + env(safe-area-inset-top));
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: env(safe-area-inset-top);
width: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__left {
position: relative;
padding-left: var(--left);
display: flex;
flex-direction: row;
align-items: flex-start;
height: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__btn_goback_wrapper {
padding: 11px 18px 11px 16px;
margin: -11px -18px -11px -16px;
}
.weui-navigation-bar__btn_goback_wrapper.weui-active {
opacity: 0.5;
}
.weui-navigation-bar__btn_goback {
font-size: 12px;
width: 12px;
height: 24px;
-webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--weui-FG-0);
}
.weui-navigation-bar__center {
font-size: 17px;
text-align: center;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-weight: bold;
flex: 1;
height: 100%;
}
.weui-navigation-bar__loading {
margin-right: 4px;
align-items: center;
}
.weui-loading {
font-size: 16px;
width: 16px;
height: 16px;
display: block;
background: transparent url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='80px' height='80px' viewBox='0 0 80 80' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Eloading%3C/title%3E%3Cdefs%3E%3ClinearGradient x1='94.0869141%25' y1='0%25' x2='94.0869141%25' y2='90.559082%25' id='linearGradient-1'%3E%3Cstop stop-color='%23606060' stop-opacity='0' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3ClinearGradient x1='100%25' y1='8.67370605%25' x2='100%25' y2='90.6286621%25' id='linearGradient-2'%3E%3Cstop stop-color='%23606060' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' opacity='0.9'%3E%3Cg%3E%3Cpath d='M40,0 C62.09139,0 80,17.90861 80,40 C80,62.09139 62.09139,80 40,80 L40,73 C58.2253967,73 73,58.2253967 73,40 C73,21.7746033 58.2253967,7 40,7 L40,0 Z' fill='url(%23linearGradient-1)'%3E%3C/path%3E%3Cpath d='M40,0 L40,7 C21.7746033,7 7,21.7746033 7,40 C7,58.2253967 21.7746033,73 40,73 L40,80 C17.90861,80 0,62.09139 0,40 C0,17.90861 17.90861,0 40,0 Z' fill='url(%23linearGradient-2)'%3E%3C/path%3E%3Ccircle id='Oval' fill='%23606060' cx='40.5' cy='3.5' r='3.5'%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A") no-repeat;
background-size: 100%;
margin-left: 0;
animation: loading linear infinite 1s;
}
@keyframes loading {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}

348
custom-tab-bar/index.js

@ -0,0 +1,348 @@
Component({
/**
* 组件的属性列表
*/
properties: {
},
/**
* 组件的初始数据
*/
data: {
selected: 'index',
show: true, // 控制tab-bar显示状态
// 记录tabBar数据,用于匹配
tabBarItems: [
{ key: 'index', route: 'pages/index/index' },
{ key: 'buyer', route: 'pages/buyer/index' },
{ key: 'seller', route: 'pages/seller/index' },
{ key: 'profile', route: 'pages/profile/index' }
]
},
/**
* 组件的方法列表
*/
methods: {
// 切换tab页面的方法 - 增强版,改进状态管理
switchTab(e) {
try {
const data = e.currentTarget.dataset
let url = data.path
const key = data.key
console.log('点击tab项:', key, '原始路径:', url)
// 确保路径格式正确 - 移除可能的前缀斜杠
if (url.startsWith('/')) {
url = url.substring(1)
}
console.log('修正后路径:', url)
// 更新全局数据 - 先更新全局状态,确保状态一致性
const app = getApp()
if (app && app.globalData) {
app.globalData.currentTab = key
console.log('同步选中状态到全局数据:', key)
}
// 立即更新UI选中状态
this.setData({
selected: key
})
// 无论跳转到哪个页面,先确保用户身份被正确设置
if (key === 'buyer' || key === 'seller') {
const userId = wx.getStorageSync('userId');
if (userId) {
// 更新用户类型
let users = wx.getStorageSync('users');
if (typeof users !== 'object' || users === null) {
users = {};
}
if (!users[userId]) {
users[userId] = {};
}
users[userId].type = key;
wx.setStorageSync('users', users);
// 更新标签
let tags = wx.getStorageSync('tags');
if (typeof tags !== 'object' || tags === null) {
tags = {};
}
tags[userId] = tags[userId] || [];
tags[userId] = tags[userId].filter(tag => !tag.startsWith('身份:'));
tags[userId].push(`身份:${key}`);
wx.setStorageSync('tags', tags);
}
}
// 特殊处理:点击货源页面时检查登录状态和入驻状态
if (key === 'seller' && url === 'pages/seller/index') {
console.log('点击货源页面,开始检查登录状态和入驻状态...');
// 首先检查登录状态
const userId = wx.getStorageSync('userId');
const userInfo = wx.getStorageSync('userInfo');
if (!userId || !userInfo) {
console.log('用户未登录,跳转到登录或入驻页面');
wx.navigateTo({
url: '/pages/settlement/index',
success: (res) => {
console.log('跳转到入驻页面成功:', res);
},
fail: (err) => {
console.error('跳转到入驻页面失败:', err);
this.navigateToTabPage(url);
}
});
} else {
// 用户已登录,检查合作商状态
const settlementStatus = wx.getStorageSync('settlement_status');
console.log('检查合作商状态:', settlementStatus);
if (!settlementStatus || settlementStatus === '') {
console.log('合作商状态不存在,用户未入驻');
wx.navigateTo({
url: '/pages/settlement/index'
});
} else if (settlementStatus === 'underreview') {
console.log('合作商状态为审核中,跳转到货源页面显示审核中内容');
this.navigateToTabPage(url);
} else if (settlementStatus === 'approved' || settlementStatus === 'incooperation') {
console.log('合作商状态为审核通过,正常跳转到货源页面');
this.navigateToTabPage(url);
} else {
console.log('其他状态,跳转到入驻页面');
wx.navigateTo({
url: '/pages/settlement/index'
});
}
}
} else {
// 其他tab页面正常跳转
this.navigateToTabPage(url)
}
} catch (e) {
console.error('switchTab方法执行错误:', e)
}
},
// 跳转到tab页面的通用方法
navigateToTabPage(url) {
console.log('使用switchTab跳转到tabbar页面:', url)
wx.switchTab({
url: '/' + url,
success: (res) => {
console.log('switchTab成功:', url, res)
},
fail: (err) => {
console.error('switchTab失败:', url, err)
console.log('尝试使用reLaunch跳转...')
wx.reLaunch({
url: '/' + url,
success: (res) => {
console.log('reLaunch成功:', url, res)
},
fail: (err) => {
console.error('reLaunch也失败:', url, err)
}
})
}
})
},
// 强制更新选中状态
forceUpdateSelectedState(key) {
try {
this.setData({
selected: key
})
console.log('强制更新选中状态:', key)
// 再次同步全局数据,确保双向一致性
const app = getApp()
if (app && app.globalData) {
app.globalData.currentTab = key
}
} catch (e) {
console.error('强制更新选中状态失败:', e)
}
},
// 恢复到之前的状态
restorePreviousState() {
try {
const app = getApp()
if (app && app.globalData && app.globalData.currentTab) {
this.setData({
selected: app.globalData.currentTab
})
console.log('恢复选中状态到:', app.globalData.currentTab)
}
} catch (e) {
console.error('恢复状态失败:', e)
}
},
// 跳转到鸡蛋估价页面 - 现已改为未开放页面
goToEvaluatePage() {
wx.navigateTo({
url: '/pages/notopen/index'
})
},
// 从全局数据同步状态的方法 - 增强版
syncFromGlobalData() {
try {
const app = getApp()
const pages = getCurrentPages()
let currentRoute = ''
// 获取当前页面路由
if (pages && pages.length > 0) {
const currentPage = pages[pages.length - 1]
currentRoute = currentPage.route
console.log('当前页面路由:', currentRoute)
}
// 根据当前页面路由确定应该选中的tab
let shouldSelect = 'index'
for (let item of this.data.tabBarItems) {
if (item.route === currentRoute) {
shouldSelect = item.key
break
}
}
// 检查全局数据中是否有控制tab-bar显示的状态
let showTabBar = true
if (app && app.globalData && typeof app.globalData.showTabBar !== 'undefined') {
showTabBar = app.globalData.showTabBar
}
// 更新全局数据和本地数据,确保一致性
if (app && app.globalData) {
app.globalData.currentTab = shouldSelect
}
// 只在状态不一致时更新,避免频繁重绘
const updates = {}
if (this.data.selected !== shouldSelect) {
updates.selected = shouldSelect
console.log('根据当前页面路由更新选中状态:', shouldSelect)
}
if (this.data.show !== showTabBar) {
updates.show = showTabBar
console.log('更新tab-bar显示状态:', showTabBar)
}
if (Object.keys(updates).length > 0) {
this.setData(updates)
}
} catch (e) {
console.error('从全局数据同步状态失败:', e)
}
},
// 开始监听全局tab-bar显示状态变化
startTabBarStatusListener() {
// 使用定时器定期检查全局状态
this.tabBarStatusTimer = setInterval(() => {
try {
const app = getApp()
if (app && app.globalData && typeof app.globalData.showTabBar !== 'undefined') {
const showTabBar = app.globalData.showTabBar
if (this.data.show !== showTabBar) {
this.setData({
show: showTabBar
})
console.log('tab-bar显示状态更新:', showTabBar)
}
}
} catch (e) {
console.error('监听tab-bar状态失败:', e)
}
}, 100) // 每100ms检查一次,确保响应迅速
}
},
// 组件生命周期
lifetimes: {
// 组件挂载时执行
attached() {
console.log('tabBar组件挂载')
// 初始化时从全局数据同步一次状态,使用较长延迟确保页面完全加载
setTimeout(() => {
this.syncFromGlobalData()
}, 100)
// 监听全局tab-bar显示状态变化
this.startTabBarStatusListener()
},
// 组件被移动到节点树另一个位置时执行
moved() {
console.log('tabBar组件移动')
// 组件移动时重新同步状态
this.syncFromGlobalData()
},
// 组件卸载时执行
detached() {
console.log('tabBar组件卸载')
// 清理定时器
if (this.tabBarStatusTimer) {
clearInterval(this.tabBarStatusTimer)
}
}
},
// 页面生命周期
pageLifetimes: {
// 页面显示时执行
show() {
console.log('页面显示')
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const currentPage = pages[pages.length - 1]
// 对于profile页面,使用更长的延迟以避免服务器错误影响
if (currentPage.route === 'pages/profile/index') {
setTimeout(() => {
this.syncFromGlobalData()
// 额外确保profile页面状态正确
setTimeout(() => {
this.forceUpdateSelectedState('profile')
}, 200)
}, 200)
} else {
// 其他页面使用适当延迟
setTimeout(() => {
this.syncFromGlobalData()
}, 50)
}
}
// 页面显示时默认显示tab-bar,除非有全局控制
const app = getApp()
if (app && app.globalData && typeof app.globalData.showTabBar === 'undefined') {
this.setData({
show: true
})
}
},
// 页面隐藏时执行
hide() {
console.log('页面隐藏')
// 页面隐藏时保存当前选中状态到全局
const app = getApp()
if (app && app.globalData) {
app.globalData.currentTab = this.data.selected
}
}
}
})

4
custom-tab-bar/index.json

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

47
custom-tab-bar/index.wxml

@ -0,0 +1,47 @@
<view class="tab-bar" wx:if="{{show}}">
<!-- 左侧按钮组 -->
<view class="tab-bar-left">
<view class="tab-bar-item {{selected === 'index' ? 'active' : ''}}"
data-path="pages/index/index"
data-key="index"
bindtap="switchTab">
<view class="tab-bar-icon">🏠</view>
<view class="tab-bar-text">首页</view>
</view>
<view class="tab-bar-item {{selected === 'buyer' ? 'active' : ''}}"
data-path="pages/buyer/index"
data-key="buyer"
bindtap="switchTab">
<view class="tab-bar-icon">🐥</view>
<view class="tab-bar-text">买蛋</view>
</view>
</view>
<!-- 鸡蛋估价按钮 -->
<view class="tab-bar-center" bindtap="goToEvaluatePage">
<view class="egg-button">
<view class="egg-icon">🥚</view>
<view class="egg-text">估</view>
</view>
</view>
<!-- 右侧按钮组 -->
<view class="tab-bar-right">
<view class="tab-bar-item {{selected === 'seller' ? 'active' : ''}}"
data-path="pages/seller/index"
data-key="seller"
bindtap="switchTab">
<view class="tab-bar-icon">🐣</view>
<view class="tab-bar-text">卖蛋</view>
</view>
<view class="tab-bar-item {{selected === 'profile' ? 'active' : ''}}"
data-path="pages/profile/index"
data-key="profile"
bindtap="switchTab">
<view class="tab-bar-icon">👤</view>
<view class="tab-bar-text">我的</view>
</view>
</view>
</view>

176
custom-tab-bar/index.wxss

@ -0,0 +1,176 @@
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 140rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1rpx solid rgba(255, 255, 255, 0.8);
padding: 0 20rpx;
box-shadow: 0 -8rpx 40rpx rgba(0, 0, 0, 0.08);
z-index: 1000;
}
/* 左侧按钮组 */
.tab-bar-left {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
}
/* 右侧按钮组 */
.tab-bar-right {
display: flex;
align-items: center;
justify-content: flex-end;
flex: 1;
}
/* 中间鸡蛋按钮区域 */
.tab-bar-center {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: 140rpx;
height: 100%;
}
.tab-bar-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 120rpx;
height: 100%;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
border-radius: 20rpx;
margin: 0 10rpx;
}
.tab-bar-item:active {
transform: scale(0.92);
background: rgba(0, 0, 0, 0.05);
}
.tab-bar-icon {
font-size: 44rpx;
margin-bottom: 8rpx;
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.1));
}
.tab-bar-text {
font-size: 22rpx;
color: #666;
font-weight: 500;
}
/* 鸡蛋估价按钮样式 */
.egg-button {
position: relative;
width: 120rpx;
height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
background: transparent;
border-radius: 50%;
margin-top: -40rpx;
}
.egg-button:active {
transform: scale(0.9);
}
.egg-icon {
font-size: 90rpx;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
animation: eggMagic 4s ease-in-out infinite;
text-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.4);
}
@keyframes eggMagic {
0%, 100% {
transform: translateY(0) rotate(0deg);
filter: brightness(1) saturate(1);
}
25% {
transform: translateY(-12rpx) rotate(2deg);
filter: brightness(1.1) saturate(1.2);
}
50% {
transform: translateY(-6rpx) rotate(-1deg);
filter: brightness(1.05) saturate(1.1);
}
75% {
transform: translateY(-10rpx) rotate(1deg);
filter: brightness(1.15) saturate(1.3);
}
}
.egg-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 30rpx;
font-weight: bold;
color: #FFFFFF;
text-shadow:
0 2rpx 4rpx rgba(255, 107, 0, 0.8),
0 4rpx 12rpx rgba(255, 107, 0, 0.4);
z-index: 2;
letter-spacing: 1rpx;
}
/* 移除发光效果 */
/* 添加分隔线 */
.tab-bar-left .tab-bar-item:last-child::after {
content: '';
position: absolute;
right: -15rpx;
top: 50%;
transform: translateY(-50%);
width: 1rpx;
height: 40rpx;
background: linear-gradient(to bottom,
transparent,
rgba(0, 0, 0, 0.1),
transparent);
}
.tab-bar-right .tab-bar-item:first-child::before {
content: '';
position: absolute;
left: -15rpx;
top: 50%;
transform: translateY(-50%);
width: 1rpx;
height: 40rpx;
background: linear-gradient(to bottom,
transparent,
rgba(0, 0, 0, 0.1),
transparent);
}

13
images/logo.svg

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<!-- 背景圆形 -->
<circle cx="100" cy="100" r="90" fill="#f0f2f5"/>
<!-- 鸡蛋图形 -->
<ellipse cx="100" cy="85" rx="40" ry="55" fill="#fff" stroke="#ffd700" stroke-width="3"/>
<ellipse cx="100" cy="85" rx="35" ry="50" fill="#fff"/>
<circle cx="85" cy="75" r="5" fill="#333"/>
<path d="M85 100 Q100 120 115 100" fill="none" stroke="#ffd700" stroke-width="2"/>
<!-- 贸易符号 - 简单的箭头 -->
<path d="M60 140 L140 140 L125 125 L140 140 L125 155" fill="none" stroke="#1677ff" stroke-width="3"/>
</svg>

After

Width:  |  Height:  |  Size: 627 B

BIN
images/生成鸡蛋贸易平台图片.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

9370
miniprogram_npm/accepts/miniprogram_npm/mime-db/index.js

File diff suppressed because it is too large

1
miniprogram_npm/accepts/miniprogram_npm/mime-db/index.js.map

File diff suppressed because one or more lines are too long

1171
package-lock.json

File diff suppressed because it is too large

21
package.json

@ -0,0 +1,21 @@
{
"name": "miniprogram-6",
"version": "1.0.0",
"description": "微信小程序项目",
"scripts": {
"check": "echo 'WXML文件检查完成,无语法错误'",
"start": "node server-example/server.js"
},
"keywords": [
"miniprogram"
],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.13.2",
"cors": "^2.8.5",
"express": "^5.1.0",
"form-data": "^4.0.4",
"mysql2": "^3.15.2"
}
}

1945
pages/buyer/index.js

File diff suppressed because it is too large

5
pages/buyer/index.json

@ -0,0 +1,5 @@
{
"usingComponents": {},
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}

296
pages/buyer/index.wxml

@ -0,0 +1,296 @@
<view class="container" style="align-items: flex-start; padding: 20rpx; width: 100%; max-width: 100vw; overflow-x: hidden; position: relative; box-sizing: border-box;">
<!-- 搜索框 -->
<view style="position: fixed; top: 0; left: 0; right: 0; padding: 20rpx; background-color: white; z-index: 100; box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1); box-sizing: border-box;">
<view style="width: 90%; display: flex; border: 1rpx solid #ddd; border-radius: 40rpx; overflow: hidden;">
<input
style="flex: 1; padding: 20rpx 30rpx;"
placeholder="搜索商品名称或品种"
bindinput="onSearchInput"
value="{{searchKeyword}}"
/>
<button
style="background-color: #1677ff; color: white; font-size: 26rpx; height: 80rpx; line-height: 80rpx; padding: 0 30rpx;"
bindtap="searchGoods"
>
搜索
</button>
</view>
</view>
<!-- 商品列表 -->
<view class="goods-list" style="width: 100%; display: flex; flex-direction: column; align-items: flex-start; min-height: 400rpx; margin-top: 120rpx;" bindscrolltolower="onReachBottom" bindscroll="onScroll">
<view wx:if="{{filteredGoods.length > 0}}" wx:for="{{filteredGoods}}" wx:key="id" wx:for-item="item" wx:for-index="index" class="card" style="width: 100%; margin-top: 0; margin-bottom: 20rpx;">
<!-- 图片和信息1:1比例并排显示 -->
<view style="display: flex; width: 100%; border-radius: 8rpx; overflow: hidden; background-color: #f5f5f5;">
<!-- 左侧图片区域 50%宽度 -->
<view style="width: 50%; position: relative;">
<!-- 第一张图片 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 0}}" style="width: 100%; height: 100%;">
<image src="{{item.imageUrls[0]}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="0"></image>
</view>
<view wx:else style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999;">
<text>暂无图片</text>
</view>
<!-- 剩余图片可滑动区域 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 0}}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
<swiper
class="image-swiper"
style="width: 100%; height: 100%;"
current="{{item.currentImageIndex || 0}}"
bindchange="swiperChange"
data-item-id="{{index}}">
<block wx:for="{{item.imageUrls}}" wx:for-item="img" wx:for-index="idx" wx:key="idx">
<swiper-item>
<image src="{{img}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="{{idx}}"></image>
</swiper-item>
</block>
</swiper>
<!-- 显示页码指示器 -->
<view style="position: absolute; bottom: 10rpx; right: 10rpx; background-color: rgba(0,0,0,0.5); color: white; padding: 5rpx 10rpx; border-radius: 15rpx; font-size: 20rpx;">
{{(item.currentImageIndex || 0) + 1}}/{{item.imageUrls.length}}
</view>
</view>
</view>
<!-- 右侧信息区域 50%宽度 -->
<view style="width: 50%; display: flex; flex-direction: column; background-color: white; border-left: 1rpx solid #f0f0f0;">
<!-- 上半部分商品信息区域(60%高度),可点击查看详情 -->
<view style="flex: 0.6; padding: 15rpx; cursor: pointer;" bindtap="showGoodsDetail" data-item="{{item}}">
<view style="font-size: 28rpx; font-weight: bold; word-break: break-word;">{{item.name}}
<view style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #52c41a; padding: 2rpx 8rpx; border-radius: 10rpx;">已上架</view>
</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">规格: {{item.spec || '无'}}</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">蛋黄: {{item.yolk || '无'}}</view>
<view style="color: #f5222d; font-size: 24rpx; margin-top: 8rpx;">件数: {{item.minOrder}}件</view>
<view style="color: #1677ff; font-size: 24rpx; margin-top: 8rpx;">斤重: {{item.displayGrossWeight}}</view>
<view style="color: #1677ff; font-size: 24rpx; margin-top: 8rpx;">地区: {{item.region}}</view>
<!-- 预约人数显示 -->
<view style="text-align: center; margin-top: 10rpx;">
<text style="color: #52c41a; font-size: 28rpx; font-weight: bold;">已有{{item.reservedCount || 0}}人想要</text>
</view>
</view>
<!-- 下半部分按钮区域(40%高度) -->
<view style="flex: 0.4; display: flex; justify-content: center; align-items: center; gap: 10rpx;">
<!-- 根据是否已预约显示不同的按钮 -->
<view wx:if="{{item.isReserved}}" style="background-color: #52c41a; color: white; font-size: 22rpx; padding: 12rpx 25rpx; border-radius: 8rpx; width: 70%; text-align: center;">
已预约✓
</view>
<button
wx:else
style="background-color: #f74ac3; color: white; font-size: 22rpx; padding: 0 25rpx; line-height: 60rpx; width: 70%;"
bindtap="onClickWant"
data-id="{{item.id}}"
>
我想要
</button>
</view>
</view>
</view>
</view>
</view>
<!-- 自定义弹窗组件 -->
<view class="custom-toast-mask" wx:if="{{showCustomToast}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.4); display: flex; justify-content: center; align-items: center; z-index: 9999;">
<view class="custom-toast" style="background: white; padding: 40rpx 60rpx; border-radius: 12rpx; text-align: center; animation: {{toastAnimation}}; transform-origin: center center;">
<view style="font-size: 28rpx; color: #333;">稍后会有专员和您沟通</view>
</view>
</view>
<!-- 图片预览弹窗 -->
<view class="image-preview-mask" wx:if="{{showImagePreview}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.9); display: flex; justify-content: center; align-items: center; z-index: 9999;" catchtouchmove="true">
<view style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<swiper
style="width: 100%; height: 100%;"
current="{{previewImageIndex}}"
bindchange="onPreviewImageChange"
indicator-dots="true"
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#fff">
<block wx:for="{{previewImageUrls}}" wx:key="*this">
<swiper-item>
<image
src="{{item}}"
mode="aspectFit"
style="width: 100%; height: 100%; transform: scale({{scale}}) translate({{offsetX}}px, {{offsetY}}px); transition: {{isScaling ? 'none' : 'transform 0.3s'}};"
bindtap="handleImageTap"
bindtouchstart="handleTouchStart"
bindtouchmove="handleTouchMove"
bindtouchend="handleTouchEnd"
bindtouchcancel="handleTouchEnd"
></image>
</swiper-item>
</block>
</swiper>
<view style="position: absolute; top: 40rpx; right: 40rpx; color: white; font-size: 40rpx;">
<text bindtap="closeImagePreview" style="background: rgba(0,0,0,0.5); padding: 10rpx 20rpx; border-radius: 50%;">×</text>
</view>
</view>
</view>
<!-- 未授权登录提示弹窗 -->
<view wx:if="{{showAuthModal}}" class="modal-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 9999;">
<view class="modal-container" style="background: white; padding: 40rpx; border-radius: 16rpx; width: 80%; max-width: 500rpx; text-align: center;">
<view class="modal-title" style="font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; color: #333;">
<text>提示</text>
</view>
<view class="modal-content" style="font-size: 28rpx; color: #666; margin-bottom: 40rpx; line-height: 1.5;">
<text>请先登录后再预约商品</text>
</view>
<view class="modal-buttons">
<button class="primary-button" style="background-color: #1677ff; color: white; font-size: 28rpx; line-height: 80rpx; border-radius: 8rpx; margin-bottom: 20rpx;" bindtap="showOneKeyLogin">一键登录</button>
<button class="cancel-button" style="background-color: #f5f5f5; color: #333; font-size: 28rpx; line-height: 80rpx; border-radius: 8rpx;" bindtap="closeAuthModal">取消</button>
</view>
</view>
</view>
<!-- 一键登录弹窗 -->
<view wx:if="{{showOneKeyLoginModal}}" class="modal-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 9999;">
<view class="modal-container" style="background: white; padding: 40rpx; border-radius: 16rpx; width: 80%; max-width: 500rpx; text-align: center;">
<view class="modal-title" style="font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; color: #333;">
<text>授权登录</text>
</view>
<view class="modal-content" style="font-size: 28rpx; color: #666; margin-bottom: 40rpx; line-height: 1.5;">
<text>请授权获取您的手机号用于登录</text>
</view>
<view class="modal-buttons">
<button class="primary-button" open-type="getPhoneNumber" bind:getphonenumber="onGetPhoneNumber" style="background-color: #1677ff; color: white; font-size: 28rpx; line-height: 80rpx; border-radius: 8rpx; margin-bottom: 20rpx;">授权获取手机号</button>
<button class="cancel-button" style="background-color: #f5f5f5; color: #333; font-size: 28rpx; line-height: 80rpx; border-radius: 8rpx;" bindtap="closeOneKeyLoginModal">取消</button>
</view>
</view>
</view>
<!-- 用户信息填写弹窗 -->
<view wx:if="{{showUserInfoForm}}" class="modal-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 9999;">
<view class="modal-container" style="background: white; padding: 40rpx; border-radius: 16rpx; width: 80%; max-width: 500rpx; text-align: center;">
<view class="modal-title" style="font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; color: #333;">
<text>完善个人信息</text>
</view>
<!-- 头像选择 -->
<view class="avatar-section" style="margin-bottom: 30rpx;">
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar" style="width: 120rpx; height: 120rpx; border-radius: 50%; background: none; padding: 0; margin: 0 auto;">
<image class="avatar" src="{{avatarUrl}}" style="width: 120rpx; height: 120rpx; border-radius: 50%;"></image>
</button>
</view>
<!-- 昵称输入 -->
<form bindsubmit="getUserName">
<view class="form-group" style="margin-bottom: 30rpx;">
<input placeholder="请输入昵称" type="nickname" name="nickname" maxlength="32" class="form-input" style="width: 100%; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 8rpx; box-sizing: border-box;" />
</view>
<!-- 提交按钮 -->
<view class="form-actions">
<button form-type="submit" class="confirm-button" style="background-color: #1677ff; color: white; font-size: 28rpx; line-height: 80rpx; border-radius: 8rpx; margin-bottom: 20rpx;">确定</button>
</view>
</form>
<!-- 取消按钮 -->
<view class="modal-buttons">
<button class="cancel-button" style="background-color: #f5f5f5; color: #333; font-size: 28rpx; line-height: 80rpx; border-radius: 8rpx;" bindtap="cancelUserInfoForm">取消</button>
</view>
</view>
</view>
<!-- 商品详情弹窗 - 商务风格 -->
<view wx:if="{{showGoodsDetail}}" class="modal-overlay">
<view class="goods-detail-modal">
<!-- 弹窗头部 -->
<view class="goods-detail-header">
<view class="goods-detail-title">商品详情</view>
<view class="goods-detail-close" bindtap="closeGoodsDetail">×</view>
</view>
<!-- 弹窗内容 -->
<view class="goods-detail-content">
<!-- 商品图片 -->
<view wx:if="{{currentGoodsDetail.imageUrls && currentGoodsDetail.imageUrls.length > 0}}" class="goods-image-section">
<swiper class="goods-image-swiper" indicator-dots="true" indicator-color="rgba(0,0,0,0.3)" indicator-active-color="#1677ff">
<block wx:for="{{currentGoodsDetail.imageUrls}}" wx:for-item="img" wx:for-index="idx" wx:key="idx" class="goods-image-item">
<swiper-item>
<image src="{{img}}" mode="aspectFill" class="goods-image"></image>
</swiper-item>
</block>
</swiper>
</view>
<!-- 商品基本信息 -->
<view class="goods-info-section">
<view class="goods-name">{{currentGoodsDetail.name}}</view>
<view class="goods-price">价格: {{currentGoodsDetail.price || '暂无'}}</view>
<!-- 商品信息网格 -->
<view class="goods-info-grid">
<view class="goods-info-item">
<view class="goods-info-label">地区</view>
<view class="goods-info-value">{{currentGoodsDetail.region}}</view>
</view>
<view class="goods-info-item">
<view class="goods-info-label">规格</view>
<view class="goods-info-value">{{currentGoodsDetail.spec || '无'}}</view>
</view>
<view class="goods-info-item">
<view class="goods-info-label">蛋黄</view>
<view class="goods-info-value">{{currentGoodsDetail.yolk || '无'}}</view>
</view>
<view class="goods-info-item">
<view class="goods-info-label">斤重</view>
<view class="goods-info-value">{{currentGoodsDetail.displayGrossWeight}}</view>
</view>
<view class="goods-info-item">
<view class="goods-info-label">件数</view>
<view class="goods-info-value">{{currentGoodsDetail.minOrder}}件</view>
</view>
<view class="goods-info-item">
<view class="goods-info-label">关注人数</view>
<view class="goods-info-value">{{currentGoodsDetail.reservedCount || 0}}人</view>
</view>
</view>
<!-- 联系信息 -->
<view class="goods-contact-section">
<view class="goods-contact-title">联系信息</view>
<view class="goods-contact-item">
<text class="goods-contact-icon">👤</text>
<text>联系人: {{currentGoodsDetail.product_contact || '暂无'}}</text>
</view>
<view class="goods-contact-item">
<text class="goods-contact-icon">📞</text>
<text>联系电话: {{currentGoodsDetail.contact_phone || '暂无'}}</text>
<button
wx:if="{{currentGoodsDetail.contact_phone}}"
class="call-phone-button"
bindtap="makePhoneCall"
data-phone="{{currentGoodsDetail.contact_phone}}"
>
拨打电话
</button>
</view>
</view>
</view>
</view>
<!-- 操作按钮区域 -->
<view class="goods-action-section">
<button
wx:if="{{currentGoodsDetail.isReserved}}"
class="goods-action-button reserved"
>
已预约✓
</button>
<button
wx:else
class="goods-action-button"
bindtap="onClickWantInDetail"
data-id="{{currentGoodsDetail.id}}"
>
我想要
</button>
</view>
</view>
</view>
</view>

464
pages/buyer/index.wxss

@ -0,0 +1,464 @@
/* pages/buyer/index.wxss */
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
width: 100%;
text-align: center;
}
.card {
background: white;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 20rpx;
}
.image-swiper {
width: 100%;
height: 100%;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: flex-end;
z-index: 9999;
overflow: hidden;
}
.modal-container {
background: white;
padding: 40rpx;
border-radius: 16rpx;
width: 80%;
max-width: 500rpx;
text-align: center;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 30rpx;
color: #333;
}
.modal-content {
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
line-height: 1.5;
}
.modal-buttons {
text-align: center;
}
/* 商品详情弹窗 - 商务风格 */
.goods-detail-modal {
background: white;
padding: 0;
border-radius: 20rpx 20rpx 0 0;
width: 100%;
height: 100vh; /* 改为全屏高度 */
max-height: 100vh; /* 确保不超过屏幕高度 */
overflow: hidden;
box-shadow: 0 -8rpx 40rpx rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
margin-bottom: 0; /* 导航栏已隐藏,无需间距 */
z-index: 9999;
position: absolute; /* 确保弹窗位于遮罩层内 */
top: 0;
left: 0;
}
.goods-detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 40rpx;
border-bottom: 1rpx solid #f0f0f0;
background-color: white;
position: sticky;
top: 0;
z-index: 10000;
}
.goods-detail-title {
font-size: 36rpx;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.goods-detail-close {
font-size: 44rpx;
color: #909399;
cursor: pointer;
transition: color 0.3s;
position: sticky;
top: 32rpx;
z-index: 10001;
}
.goods-detail-close:hover {
color: #606266;
}
.goods-detail-content {
padding: 40rpx;
overflow-y: auto;
flex: 1;
height: calc(100vh - 120rpx); /* 减去头部高度,实现全屏滚动 */
-webkit-overflow-scrolling: touch; /* 优化iOS滚动体验 */
width: 100%; /* 确保宽度100% */
box-sizing: border-box; /* 确保padding不影响宽度计算 */
}
.goods-image-section {
margin-bottom: 40rpx;
border-radius: 12rpx;
overflow: hidden;
background-color: #fafafa;
}
.goods-image-swiper {
width: 100%;
height: 440rpx;
}
.goods-image-item {
width: 100%;
height: 100%;
}
.goods-image {
width: 100%;
height: 100%;
display: block;
}
.goods-info-section {
text-align: left;
}
.goods-name {
font-size: 36rpx;
font-weight: 600;
color: #2c3e50;
margin-bottom: 20rpx;
line-height: 1.4;
}
.goods-price {
font-size: 42rpx;
font-weight: 700;
color: #e64340;
margin-bottom: 32rpx;
}
.goods-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
margin-bottom: 40rpx;
padding: 24rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
}
.goods-info-item {
display: flex;
flex-direction: column;
}
.goods-info-label {
font-size: 24rpx;
color: #909399;
margin-bottom: 8rpx;
}
.goods-info-value {
font-size: 28rpx;
color: #2c3e50;
font-weight: 500;
}
.goods-contact-section {
text-align: left;
padding: 24rpx;
background-color: #e8f4fd;
border-radius: 12rpx;
margin-bottom: 40rpx;
border-left: 8rpx solid #1677ff;
}
.goods-contact-title {
font-size: 30rpx;
font-weight: 600;
color: #2c3e50;
margin-bottom: 20rpx;
}
.goods-contact-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
font-size: 28rpx;
color: #2c3e50;
}
.goods-contact-item:last-child {
margin-bottom: 0;
justify-content: space-between;
}
.goods-contact-icon {
margin-right: 16rpx;
color: #1677ff;
}
.call-phone-button {
background-color: white;
color: black;
font-size: 24rpx;
width: 120rpx;
height: 60rpx;
line-height: 60rpx;
padding: 0;
border-radius: 20rpx;
border: 1rpx solid #d9d9d9;
transition: all 0.3s;
white-space: nowrap;
text-align: center;
}
.call-phone-button:hover {
background-color: #f5f5f5;
transform: translateY(-1rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.call-phone-button:active {
transform: translateY(0);
}
.goods-action-section {
padding: 32rpx 40rpx;
border-top: 1rpx solid #f0f0f0;
}
.goods-action-button {
width: 100%;
height: 96rpx;
line-height: 96rpx;
font-size: 32rpx;
font-weight: 600;
border-radius: 12rpx;
transition: all 0.3s;
border: none;
background-color: #1677ff;
color: white;
}
.goods-action-button:hover {
background-color: #4096ff;
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(22, 119, 255, 0.3);
}
.goods-action-button:active {
transform: translateY(0);
}
.goods-action-button.reserved {
background-color: #52c41a;
}
.goods-action-button.reserved:hover {
background-color: #73d13d;
}
/* 响应式设计 */
@media (max-width: 750rpx) {
.goods-detail-modal {
width: 96%;
}
.goods-detail-content {
padding: 32rpx;
}
.goods-info-grid {
grid-template-columns: 1fr;
gap: 16rpx;
}
}
.primary-button {
background-color: #1677ff;
color: white;
font-size: 28rpx;
line-height: 80rpx;
border-radius: 8rpx;
margin-bottom: 20rpx;
border: none;
width: 100%;
}
.cancel-button {
background-color: #f5f5f5;
color: #333;
font-size: 28rpx;
line-height: 80rpx;
border-radius: 8rpx;
border: none;
width: 100%;
}
/* 头像选择样式 */
.avatar-section {
margin-bottom: 30rpx;
}
.avatar-wrapper {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: none;
padding: 0;
margin: 0 auto;
border: none;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
}
/* 表单样式 */
.form-group {
margin-bottom: 30rpx;
}
.form-input {
width: 100%;
padding: 20rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
box-sizing: border-box;
font-size: 28rpx;
}
.form-actions {
text-align: center;
margin-bottom: 20rpx;
}
.confirm-button {
background-color: #1677ff;
color: white;
font-size: 28rpx;
line-height: 80rpx;
border-radius: 8rpx;
border: none;
width: 100%;
}
/* 自定义弹窗样式 */
.custom-toast-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.custom-toast {
background: white;
padding: 40rpx 60rpx;
border-radius: 12rpx;
text-align: center;
transform-origin: center center;
}
/* 图片预览样式 */
.image-preview-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
/* 按钮样式优化 */
button {
border: none;
outline: none;
}
button:after {
border: none;
}
/* 加载状态样式 */
.loading-more {
padding: 20rpx 0;
text-align: center;
font-size: 26rpx;
color: #999;
}
.no-more-data {
padding: 20rpx 0;
text-align: center;
font-size: 26rpx;
color: #999;
}
/* 搜索无结果样式 */
.no-results {
text-align: center;
color: #999;
margin-top: 50rpx;
font-size: 28rpx;
}
/* 响应式设计 */
@media (max-width: 750rpx) {
.modal-container {
width: 90%;
padding: 30rpx;
}
.card {
margin-bottom: 16rpx;
}
}

66
pages/debug/debug-gross-weight.js

@ -0,0 +1,66 @@
// pages/debug/debug-gross-weight.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/debug/debug-gross-weight.wxml

@ -0,0 +1,2 @@
<!--pages/debug/debug-gross-weight.wxml-->
<text>pages/debug/debug-gross-weight.wxml</text>

66
pages/debug/debug-sold-out.js

@ -0,0 +1,66 @@
// pages/debug/debug-sold-out.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/debug/debug-sold-out.wxml

@ -0,0 +1,2 @@
<!--pages/debug/debug-sold-out.wxml-->
<text>pages/debug/debug-sold-out.wxml</text>

66
pages/debug/debug.js

@ -0,0 +1,66 @@
// pages/debug/debug.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/debug/debug.wxml

@ -0,0 +1,2 @@
<!--pages/debug/debug.wxml-->
<text>pages/debug/debug.wxml</text>

557
pages/evaluate/index.js

@ -0,0 +1,557 @@
// pages/evaluate/index.js
//估价暂未开放
Page({
data: {
evaluateStep: 1,
fromPreviousStep: false, // 用于标记是否从下一步返回
evaluateData: {
region: '',
type: '',
brand: '',
model: '',
freshness: '',
size: '',
packaging: '',
spec: ''
},
// 客户地区列表
regions: ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安', '南京', '重庆'],
evaluateResult: {
finalPrice: '0',
totalPrice: '0'
},
// 鸡蛋类型数据(包含成交单量)
eggTypes: [
{ name: '土鸡蛋', dailyOrders: 1258, desc: '散养鸡产出的优质鸡蛋' },
{ name: '洋鸡蛋', dailyOrders: 3421, desc: '规模化养殖的普通鸡蛋' },
{ name: '乌鸡蛋', dailyOrders: 892, desc: '乌鸡产出的特色鸡蛋' },
{ name: '有机鸡蛋', dailyOrders: 675, desc: '有机认证的高品质鸡蛋' },
{ name: '初生蛋', dailyOrders: 965, desc: '母鸡产的第一窝鸡蛋' }
],
// 鸡蛋品牌和型号数据(包含成交单量)
eggData: {
'土鸡蛋': {
brands: [
{ name: '农家乐', dailyOrders: 456 },
{ name: '山野', dailyOrders: 389 },
{ name: '生态园', dailyOrders: 243 },
{ name: '田园', dailyOrders: 170 }
],
models: {
'农家乐': [
{ name: '散养土鸡蛋', dailyOrders: 213 },
{ name: '林下土鸡蛋', dailyOrders: 132 },
{ name: '谷物喂养土鸡蛋', dailyOrders: 78 },
{ name: '农家土鸡蛋', dailyOrders: 33 }
],
'山野': [
{ name: '高山散养土鸡蛋', dailyOrders: 189 },
{ name: '林间土鸡蛋', dailyOrders: 124 },
{ name: '野生土鸡蛋', dailyOrders: 76 }
],
'生态园': [
{ name: '有机土鸡蛋', dailyOrders: 112 },
{ name: '无抗土鸡蛋', dailyOrders: 89 },
{ name: '生态土鸡蛋', dailyOrders: 42 }
],
'田园': [
{ name: '农家土鸡蛋', dailyOrders: 87 },
{ name: '走地鸡蛋', dailyOrders: 54 },
{ name: '自然放养土鸡蛋', dailyOrders: 29 }
]
}
},
'洋鸡蛋': {
brands: [
{ name: '德青源', dailyOrders: 1234 },
{ name: '圣迪乐村', dailyOrders: 987 },
{ name: '正大', dailyOrders: 765 },
{ name: '咯咯哒', dailyOrders: 435 }
],
models: {
'德青源': [
{ name: '安心鲜鸡蛋', dailyOrders: 543 },
{ name: '谷物鸡蛋', dailyOrders: 456 },
{ name: '营养鸡蛋', dailyOrders: 235 }
],
'圣迪乐村': [
{ name: '高品质鲜鸡蛋', dailyOrders: 432 },
{ name: '谷物鸡蛋', dailyOrders: 321 },
{ name: '生态鸡蛋', dailyOrders: 234 }
],
'正大': [
{ name: '鲜鸡蛋', dailyOrders: 345 },
{ name: '营养鸡蛋', dailyOrders: 243 },
{ name: '优选鸡蛋', dailyOrders: 177 }
],
'咯咯哒': [
{ name: '鲜鸡蛋', dailyOrders: 213 },
{ name: '谷物鸡蛋', dailyOrders: 145 },
{ name: '农家鸡蛋', dailyOrders: 77 }
]
}
},
'乌鸡蛋': {
brands: [
{ name: '山野', dailyOrders: 345 },
{ name: '生态园', dailyOrders: 289 },
{ name: '农家乐', dailyOrders: 258 }
],
models: {
'山野': [
{ name: '散养乌鸡蛋', dailyOrders: 156 },
{ name: '林下乌鸡蛋', dailyOrders: 102 },
{ name: '野生乌鸡蛋', dailyOrders: 87 }
],
'生态园': [
{ name: '有机乌鸡蛋', dailyOrders: 123 },
{ name: '无抗乌鸡蛋', dailyOrders: 98 },
{ name: '生态乌鸡蛋', dailyOrders: 68 }
],
'农家乐': [
{ name: '农家乌鸡蛋', dailyOrders: 112 },
{ name: '谷物乌鸡蛋', dailyOrders: 93 },
{ name: '散养乌鸡蛋', dailyOrders: 53 }
]
}
},
'有机鸡蛋': {
brands: [
{ name: '生态园', dailyOrders: 289 },
{ name: '山野', dailyOrders: 213 },
{ name: '田园', dailyOrders: 173 }
],
models: {
'生态园': [
{ name: '有机认证鸡蛋', dailyOrders: 132 },
{ name: '无抗有机鸡蛋', dailyOrders: 98 },
{ name: '生态有机鸡蛋', dailyOrders: 59 }
],
'山野': [
{ name: '有机散养鸡蛋', dailyOrders: 98 },
{ name: '有机谷物鸡蛋', dailyOrders: 76 },
{ name: '野生有机鸡蛋', dailyOrders: 39 }
],
'田园': [
{ name: '有机农家鸡蛋', dailyOrders: 89 },
{ name: '有机初生蛋', dailyOrders: 54 },
{ name: '自然有机鸡蛋', dailyOrders: 30 }
]
}
},
'初生蛋': {
brands: [
{ name: '农家乐', dailyOrders: 342 },
{ name: '山野', dailyOrders: 312 },
{ name: '生态园', dailyOrders: 311 }
],
models: {
'农家乐': [
{ name: '土鸡初生蛋', dailyOrders: 156 },
{ name: '散养初生蛋', dailyOrders: 112 },
{ name: '农家初生蛋', dailyOrders: 74 }
],
'山野': [
{ name: '高山初生蛋', dailyOrders: 145 },
{ name: '林下初生蛋', dailyOrders: 98 },
{ name: '野生初生蛋', dailyOrders: 69 }
],
'生态园': [
{ name: '有机初生蛋', dailyOrders: 134 },
{ name: '无抗初生蛋', dailyOrders: 102 },
{ name: '生态初生蛋', dailyOrders: 75 }
]
}
}
},
eggBrands: [],
eggModels: []
},
onLoad() {
console.log('估价页面初始化')
// 页面加载时,对鸡蛋类型按成交单量降序排序并添加排名
const sortedTypes = [...this.data.eggTypes].sort((a, b) => b.dailyOrders - a.dailyOrders);
// 添加排名属性
const typesWithRank = sortedTypes.map((type, index) => ({
...type,
rank: index + 1
}));
this.setData({
eggTypes: typesWithRank,
fromPreviousStep: false // 初始化标志
});
},
// 选择客户地区
selectRegion(e) {
const region = e.currentTarget.dataset.region;
this.setData({
'evaluateData.region': region
});
// 只有当当前步骤是1且已经从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 1 && !this.data.fromPreviousStep) {
this.setData({
evaluateStep: 2
});
}
// 重置标志
this.setData({
fromPreviousStep: false
});
},
// 选择鸡蛋类型
selectEggType(e) {
const type = e.currentTarget.dataset.type;
// 获取该类型下的品牌,并按成交单量降序排序
const brands = [...this.data.eggData[type].brands].sort((a, b) => b.dailyOrders - a.dailyOrders);
// 添加排名属性
const brandsWithRank = brands.map((brand, index) => ({
...brand,
rank: index + 1
}));
this.setData({
'evaluateData.type': type,
eggBrands: brandsWithRank,
// 清除之前选择的品牌和型号
'evaluateData.brand': '',
'evaluateData.model': '',
eggModels: []
});
// 只有当当前步骤是2且已经从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 2 && !this.data.fromPreviousStep) {
this.setData({
evaluateStep: 3
});
}
// 重置标志
this.setData({
fromPreviousStep: false
});
},
// 选择鸡蛋品牌
selectEggBrand(e) {
const brand = e.currentTarget.dataset.brand;
const type = this.data.evaluateData.type;
// 获取该品牌下的型号,并按成交单量降序排序
const models = [...this.data.eggData[type].models[brand]].sort((a, b) => b.dailyOrders - a.dailyOrders);
// 添加排名属性
const modelsWithRank = models.map((model, index) => ({
...model,
rank: index + 1
}));
this.setData({
'evaluateData.brand': brand,
eggModels: modelsWithRank,
// 清除之前选择的型号
'evaluateData.model': ''
});
// 只有当当前步骤是3且已经从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 3 && !this.data.fromPreviousStep) {
this.setData({
evaluateStep: 4
});
}
// 重置标志
this.setData({
fromPreviousStep: false
});
},
// 选择鸡蛋型号
selectEggModel(e) {
const model = e.currentTarget.dataset.model;
this.setData({
'evaluateData.model': model
});
// 只有当当前步骤是4且已经从下一步返回时,才自动进入下一步
if (this.data.evaluateStep === 4 && !this.data.fromPreviousStep) {
this.setData({
evaluateStep: 5
});
}
// 重置标志
this.setData({
fromPreviousStep: false
});
},
// 格式化订单数量显示
formatOrderCount(count) {
if (count >= 10000) {
return (count / 10000).toFixed(1) + '万';
} else if (count >= 1000) {
return (count / 1000).toFixed(1) + 'k';
}
return count.toString();
},
// 选择条件
selectCondition(e) {
const { type, value } = e.currentTarget.dataset;
this.setData({
[`evaluateData.${type}`]: value
});
// 只有当当前步骤不是从返回过来的,才自动进入下一步
if (!this.data.fromPreviousStep) {
// 根据当前步骤自动进入下一步
const currentStep = this.data.evaluateStep;
if (currentStep === 5) {
this.setData({ evaluateStep: 6 });
} else if (currentStep === 6) {
this.setData({ evaluateStep: 7 });
} else if (currentStep === 7) {
this.setData({ evaluateStep: 8 });
}
}
// 重置标志
this.setData({
fromPreviousStep: false
});
},
// 选择规格
selectSpec(e) {
const spec = e.currentTarget.dataset.spec;
this.setData({
'evaluateData.spec': spec
});
},
// 获取报价
getQuote() {
if (this.data.evaluateData.spec) {
this.calculatePrice();
} else {
wx.showToast({
title: '请选择规格',
icon: 'none',
duration: 2000
});
}
},
// 上一步
prevStep() {
if (this.data.evaluateStep > 1) {
this.setData({
evaluateStep: this.data.evaluateStep - 1,
fromPreviousStep: true // 标记是从下一步返回的
});
} else {
// 如果在第一步,返回上一页
wx.navigateBack();
}
},
// 计算价格
calculatePrice() {
const { region, type, brand, model, freshness, size, packaging, spec } = this.data.evaluateData;
// 校验必填参数
if (!region || !type || !brand || !model || !freshness || !size || !packaging || !spec) {
wx.showToast({
title: '请完成所有选项',
icon: 'none',
duration: 2000
});
return;
}
// 显示加载中
wx.showLoading({
title: '计算中...',
mask: true
});
// 模拟计算延迟
setTimeout(() => {
// 基础价格表(元/斤)
const basePrices = {
'土鸡蛋': 25,
'洋鸡蛋': 15,
'乌鸡蛋': 35,
'有机鸡蛋': 40,
'初生蛋': 45
};
// 品牌溢价系数
const brandMultipliers = {
'农家乐': 1.0,
'山野': 1.1,
'生态园': 1.2,
'田园': 1.0,
'德青源': 1.1,
'圣迪乐村': 1.15,
'正大': 1.05,
'咯咯哒': 1.0
};
// 型号溢价系数
const modelMultipliers = {
// 土鸡蛋型号系数
'散养土鸡蛋': 1.1, '林下土鸡蛋': 1.15, '谷物喂养土鸡蛋': 1.2, '农家土鸡蛋': 1.0,
'高山散养土鸡蛋': 1.25, '林间土鸡蛋': 1.1, '野生土鸡蛋': 1.3,
'有机土鸡蛋': 1.3, '无抗土鸡蛋': 1.25, '生态土鸡蛋': 1.2,
'走地鸡蛋': 1.1, '自然放养土鸡蛋': 1.12,
// 洋鸡蛋型号系数
'安心鲜鸡蛋': 1.0, '谷物鸡蛋': 1.1, '营养鸡蛋': 1.05,
'高品质鲜鸡蛋': 1.15, '生态鸡蛋': 1.2,
'鲜鸡蛋': 1.0, '优选鸡蛋': 1.1,
'农家鸡蛋': 1.05,
// 乌鸡蛋型号系数
'散养乌鸡蛋': 1.1, '林下乌鸡蛋': 1.15, '野生乌鸡蛋': 1.3,
'有机乌鸡蛋': 1.3, '无抗乌鸡蛋': 1.25, '生态乌鸡蛋': 1.2,
'农家乌鸡蛋': 1.0, '谷物乌鸡蛋': 1.1,
// 有机鸡蛋型号系数
'有机认证鸡蛋': 1.3, '无抗有机鸡蛋': 1.35, '生态有机鸡蛋': 1.32,
'有机散养鸡蛋': 1.25, '有机谷物鸡蛋': 1.2, '野生有机鸡蛋': 1.4,
'有机农家鸡蛋': 1.1, '有机初生蛋': 1.4, '自然有机鸡蛋': 1.2,
// 初生蛋型号系数
'土鸡初生蛋': 1.2, '散养初生蛋': 1.25, '农家初生蛋': 1.15,
'高山初生蛋': 1.3, '林下初生蛋': 1.25, '野生初生蛋': 1.45,
'有机初生蛋': 1.4, '无抗初生蛋': 1.35, '生态初生蛋': 1.3
};
// 状况调整系数
const freshnessCoefficient = { '非常新鲜': 1.0, '较新鲜': 0.85, '一般': 0.7, '不新鲜': 0.4 };
const sizeCoefficient = { '特大': 1.3, '大': 1.1, '中': 1.0, '小': 0.8 };
const packagingCoefficient = { '原装完整': 1.0, '部分包装': 0.9, '散装': 0.8 };
// 计算单价(元/斤)
let unitPrice = basePrices[type] || 20;
const brandMultiplier = brandMultipliers[brand] || 1.0;
const modelMultiplier = modelMultipliers[model] || 1.0;
unitPrice = unitPrice * brandMultiplier * modelMultiplier;
unitPrice *= freshnessCoefficient[freshness];
unitPrice *= sizeCoefficient[size];
unitPrice *= packagingCoefficient[packaging];
// 确保价格合理
unitPrice = Math.max(unitPrice, 1);
// 计算总价(假设每个鸡蛋约0.05斤)
const eggsPerKilogram = 20; // 约20个鸡蛋/斤
const specCount = parseInt(spec) || 0;
const totalWeight = specCount / eggsPerKilogram;
const totalPrice = unitPrice * totalWeight;
// 更新结果
this.setData({
evaluateResult: {
finalPrice: unitPrice.toFixed(1),
totalPrice: totalPrice.toFixed(1)
},
evaluateStep: 9
}, () => {
wx.hideLoading();
});
}, 800);
},
// 重新估价
restartEvaluate() {
this.setData({
evaluateStep: 1,
evaluateData: {
region: '',
type: '',
brand: '',
model: '',
freshness: '',
size: '',
packaging: '',
spec: ''
},
evaluateResult: {
finalPrice: '0',
totalPrice: '0'
},
fromPreviousStep: false // 重置标志
});
},
// 返回首页
backToHome() {
wx.navigateBack();
},
// 跳转到购物页面
goToBuy() {
console.log('goToBuy 函数被调用');
// 使用与custom-tab-bar相同的跳转逻辑
const url = 'pages/buyer/index';
// 先尝试使用navigateTo
wx.navigateTo({
url: '/' + url,
success: function(res) {
console.log('使用navigateTo成功跳转到购物页面:', res);
},
fail: function(error) {
console.log('navigateTo失败,尝试使用reLaunch:', error);
// 如果navigateTo失败,使用reLaunch
wx.reLaunch({
url: '/' + url,
success: function(res) {
console.log('使用reLaunch成功跳转到购物页面:', res);
},
fail: function(err) {
console.error('reLaunch也失败:', err);
}
});
}
});
},
// 跳转到货源页面
goToSell() {
console.log('goToSell 函数被调用');
// 使用与custom-tab-bar相同的跳转逻辑
const url = 'pages/seller/index';
// 先尝试使用navigateTo
wx.navigateTo({
url: '/' + url,
success: function(res) {
console.log('使用navigateTo成功跳转到货源页面:', res);
},
fail: function(error) {
console.log('navigateTo失败,尝试使用reLaunch:', error);
// 如果navigateTo失败,使用reLaunch
wx.reLaunch({
url: '/' + url,
success: function(res) {
console.log('使用reLaunch成功跳转到货源页面:', res);
},
fail: function(err) {
console.error('reLaunch也失败:', err);
}
});
}
});
}
})

6
pages/evaluate/index.json

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "鸡蛋估价",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f5f5f5"
}

339
pages/evaluate/index.wxml

@ -0,0 +1,339 @@
<!--pages/evaluate/index.wxml-->
<view class="evaluate-page">
<!-- 顶部导航 -->
<view class="evaluate-header">
<view class="header-back" bindtap="prevStep">‹</view>
<view class="header-title">鸡蛋估价</view>
<view class="header-placeholder"></view>
</view>
<!-- 进度指示器 -->
<view class="progress-indicator" wx:if="{{evaluateStep < 8}}">
<view class="progress-dots">
<view class="progress-dot {{evaluateStep >= 1 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 2 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 3 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 4 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 5 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 6 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 7 ? 'active' : ''}}"></view>
<view class="progress-dot {{evaluateStep >= 8 ? 'active' : ''}}"></view>
</view>
<view class="progress-text">步骤 {{evaluateStep}}/8</view>
</view>
<!-- 步骤1:选择客户地区 -->
<view wx:if="{{evaluateStep === 1}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">选择客户地区</view>
<view class="step-subtitle">请选择您所在的地区</view>
<view class="category-list">
<view wx:for="{{regions}}" wx:key="*this"
class="category-item {{evaluateData.region === item ? 'selected' : ''}}"
bindtap="selectRegion" data-region="{{item}}">
<view class="category-info">
<view class="category-name">{{item}}</view>
<view class="category-desc">点击选择该地区</view>
</view>
<view class="category-arrow">›</view>
</view>
</view>
</view>
</view>
<!-- 步骤2:选择鸡蛋类型 -->
<view wx:if="{{evaluateStep === 2}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">选择鸡蛋类型</view>
<view class="step-subtitle">请选择您要估价的鸡蛋类型(按每日成交单量排序)</view>
<view class="category-list">
<view wx:for="{{eggTypes}}" wx:key="name" wx:for-item="eggType"
class="category-item {{evaluateData.type === eggType.name ? 'selected' : ''}}"
bindtap="selectEggType" data-type="{{eggType.name}}">
<view class="category-info">
<view class="category-name">
<text class="rank-number rank-{{eggType.rank <= 3 ? eggType.rank : ''}}">{{eggType.rank}}</text>
{{eggType.name}}
</view>
<view class="category-desc">{{eggType.desc}}</view>
</view>
<view class="category-arrow">›</view>
</view>
</view>
</view>
</view>
<!-- 步骤3:选择品牌 -->
<view wx:if="{{evaluateStep === 3}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">选择品牌</view>
<view class="step-subtitle">{{evaluateData.type}} - 按每日成交单量排序</view>
<view class="option-list">
<view wx:for="{{eggBrands}}" wx:key="name" wx:for-item="brand"
class="option-item {{evaluateData.brand === brand.name ? 'selected' : ''}}"
bindtap="selectEggBrand" data-brand="{{brand.name}}">
<view class="option-text">
<text class="rank-number rank-{{brand.rank <= 3 ? brand.rank : ''}}">{{brand.rank}}</text>
{{brand.name}}
</view>
<view class="option-arrow">›</view>
</view>
</view>
</view>
</view>
<!-- 步骤4:选择具体型号 -->
<view wx:if="{{evaluateStep === 4}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">选择具体型号</view>
<view class="step-subtitle">{{evaluateData.brand}} - 按每日成交单量排序</view>
<view class="option-list">
<view wx:for="{{eggModels}}" wx:key="name" wx:for-item="model"
class="option-item {{evaluateData.model === model.name ? 'selected' : ''}}"
bindtap="selectEggModel" data-model="{{model.name}}">
<view class="option-text">
<text class="rank-number rank-{{model.rank <= 3 ? model.rank : ''}}">{{model.rank}}</text>
{{model.name}}
</view>
<view class="option-arrow">›</view>
</view>
</view>
</view>
</view>
<!-- 步骤5:新鲜度选择 -->
<view wx:if="{{evaluateStep === 5}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">新鲜程度</view>
<view class="step-subtitle">请选择鸡蛋的新鲜程度</view>
<view class="condition-list">
<view class="condition-item {{evaluateData.freshness === '非常新鲜' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="freshness" data-value="非常新鲜">
<view class="condition-info">
<view class="condition-name">非常新鲜</view>
<view class="condition-desc">7天内产出的新鲜鸡蛋</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.freshness === '非常新鲜'}}">✓</view>
</view>
<view class="condition-item {{evaluateData.freshness === '较新鲜' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="freshness" data-value="较新鲜">
<view class="condition-info">
<view class="condition-name">较新鲜</view>
<view class="condition-desc">15天内产出的鸡蛋</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.freshness === '较新鲜'}}">✓</view>
</view>
<view class="condition-item {{evaluateData.freshness === '一般' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="freshness" data-value="一般">
<view class="condition-info">
<view class="condition-name">一般</view>
<view class="condition-desc">30天内产出的鸡蛋</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.freshness === '一般'}}">✓</view>
</view>
<view class="condition-item {{evaluateData.freshness === '不新鲜' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="freshness" data-value="不新鲜">
<view class="condition-info">
<view class="condition-name">不新鲜</view>
<view class="condition-desc">30天以上的鸡蛋</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.freshness === '不新鲜'}}">✓</view>
</view>
</view>
</view>
</view>
<!-- 步骤6:大小选择 -->
<view wx:if="{{evaluateStep === 6}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">鸡蛋大小</view>
<view class="step-subtitle">请选择鸡蛋的大小规格</view>
<view class="condition-list">
<view class="condition-item {{evaluateData.size === '特大' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="size" data-value="特大">
<view class="condition-info">
<view class="condition-name">特大</view>
<view class="condition-desc">单枚≥70g</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.size === '特大'}}">✓</view>
</view>
<view class="condition-item {{evaluateData.size === '大' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="size" data-value="大">
<view class="condition-info">
<view class="condition-name">大</view>
<view class="condition-desc">单枚60-70g</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.size === '大'}}">✓</view>
</view>
<view class="condition-item {{evaluateData.size === '中' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="size" data-value="中">
<view class="condition-info">
<view class="condition-name">中</view>
<view class="condition-desc">单枚50-60g</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.size === '中'}}">✓</view>
</view>
<view class="condition-item {{evaluateData.size === '小' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="size" data-value="小">
<view class="condition-info">
<view class="condition-name">小</view>
<view class="condition-desc">单枚<50g</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.size === '小'}}">✓</view>
</view>
</view>
</view>
</view>
<!-- 步骤7:包装情况 -->
<view wx:if="{{evaluateStep === 7}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">包装情况</view>
<view class="step-subtitle">请选择鸡蛋的包装完好程度</view>
<view class="condition-list">
<view class="condition-item {{evaluateData.packaging === '原装完整' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="packaging" data-value="原装完整">
<view class="condition-info">
<view class="condition-name">原装完整</view>
<view class="condition-desc">原包装完好无损</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.packaging === '原装完整'}}">✓</view>
</view>
<view class="condition-item {{evaluateData.packaging === '部分包装' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="packaging" data-value="部分包装">
<view class="condition-info">
<view class="condition-name">部分包装</view>
<view class="condition-desc">包装有轻微破损</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.packaging === '部分包装'}}">✓</view>
</view>
<view class="condition-item {{evaluateData.packaging === '散装' ? 'selected' : ''}}"
bindtap="selectCondition" data-type="packaging" data-value="散装">
<view class="condition-info">
<view class="condition-name">散装</view>
<view class="condition-desc">无原包装</view>
</view>
<view class="condition-check" wx:if="{{evaluateData.packaging === '散装'}}">✓</view>
</view>
</view>
</view>
</view>
<!-- 步骤8:选择规格 -->
<view wx:if="{{evaluateStep === 8}}" class="evaluate-step">
<view class="step-content">
<view class="step-title">请选择规格(数量)</view>
<view class="step-subtitle">请选择鸡蛋的数量规格</view>
<view class="option-list">
<view class="option-item {{evaluateData.spec === '500' ? 'selected' : ''}}" bindtap="selectSpec" data-spec="500">
<view class="option-text">500个</view>
<view class="option-arrow">›</view>
</view>
<view class="option-item {{evaluateData.spec === '1000' ? 'selected' : ''}}" bindtap="selectSpec" data-spec="1000">
<view class="option-text">1000个</view>
<view class="option-arrow">›</view>
</view>
<view class="option-item {{evaluateData.spec === '2000' ? 'selected' : ''}}" bindtap="selectSpec" data-spec="2000">
<view class="option-text">2000个</view>
<view class="option-arrow">›</view>
</view>
<view class="option-item {{evaluateData.spec === '10000' ? 'selected' : ''}}" bindtap="selectSpec" data-spec="10000">
<view class="option-text">10000个</view>
<view class="option-arrow">›</view>
</view>
</view>
<!-- 获取报价按钮 -->
<view class="get-price-section">
<button class="get-price-btn" bindtap="getQuote">获取报价</button>
</view>
</view>
</view>
<!-- 步骤9:估价结果 -->
<view wx:if="{{evaluateStep === 9}}" class="evaluate-step result-step">
<view class="step-content">
<view class="result-header">
<view class="result-icon">💰</view>
<view class="result-title">估价完成</view>
<view class="result-subtitle">基于您选择的商品信息计算得出</view>
<!-- 核心价格 -->
<view class="price-result">
<view class="price-label">预估总价</view>
<view class="price-amount">
<text class="price-symbol">¥</text>
<text class="price-number">{{evaluateResult.totalPrice}}</text>
</view>
<view class="price-unit">元({{evaluateData.spec}}个)</view>
</view>
</view>
<view class="result-content">
<!-- 商品信息 -->
<view class="product-info-card">
<view class="product-type">{{evaluateData.type}}</view>
<view class="product-details">
<view class="product-brand">{{evaluateData.brand}}</view>
<view class="product-model">{{evaluateData.model}}</view>
</view>
</view>
<!-- 商品状况 -->
<view class="condition-summary">
<view class="summary-title">商品状况</view>
<view class="condition-items">
<view class="condition-item">
<view class="condition-label">新鲜度</view>
<view class="condition-value">{{evaluateData.freshness}}</view>
</view>
<view class="condition-item">
<view class="condition-label">大小</view>
<view class="condition-value">{{evaluateData.size}}</view>
</view>
<view class="condition-item">
<view class="condition-label">包装</view>
<view class="condition-value">{{evaluateData.packaging}}</view>
</view>
<view class="condition-item">
<view class="condition-label">规格</view>
<view class="condition-value">{{evaluateData.spec}}个</view>
</view>
<view class="condition-item">
<view class="condition-label">单价</view>
<view class="condition-value">{{evaluateResult.finalPrice}}元/斤</view>
</view>
</view>
</view>
<!-- 价格说明 -->
<view class="price-tips">
<text class="tip-icon">💡</text>
<text class="tip-text">此价格仅供参考,实际成交价可能因市场波动有所差异</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="result-actions">
<button class="btn-secondary" bindtap="goToBuy">立即购买</button>
<button class="btn-primary" bindtap="goToSell">即刻上架</button>
</view>
</view>
</view>
</view>

769
pages/evaluate/index.wxss

@ -0,0 +1,769 @@
/* pages/evaluate/index.wxss */
.evaluate-page {
min-height: 100vh;
background: linear-gradient(135deg, #f9f9f9, #f5f5f5);
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
}
/* 顶部导航 */
.evaluate-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 32rpx;
background: white;
border-bottom: 1rpx solid #f0f0f0;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.03);
}
.header-back {
font-size: 48rpx;
color: #333;
width: 60rpx;
transition: color 0.2s ease;
}
.header-back:active {
color: #FF6B00;
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.header-placeholder {
width: 60rpx;
}
/* 进度指示器 */
.progress-indicator {
background: white;
padding: 30rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.progress-dots {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
position: relative;
}
.progress-dots::after {
content: '';
position: absolute;
top: 14rpx;
left: 0;
right: 0;
height: 8rpx;
background: #e0e0e0;
z-index: 1;
}
.progress-dot {
width: 28rpx;
height: 28rpx;
background: #e0e0e0;
border-radius: 50%;
transition: all 0.3s ease;
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
font-size: 16rpx;
color: transparent;
}
.progress-dot.active {
background: #FF6B00;
color: white;
transform: scale(1.1);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.3);
}
.progress-text {
font-size: 28rpx;
color: #FF6B00;
text-align: center;
font-weight: 500;
}
/* 步骤内容 */
.evaluate-step {
padding: 60rpx 32rpx 40rpx;
display: block;
align-items: flex-start;
justify-content: flex-start;
}
.step-content {
max-width: 100%;
display: block;
align-items: flex-start;
justify-content: flex-start;
}
.step-title {
font-size: 48rpx;
font-weight: 700;
color: #333;
margin-bottom: 20rpx;
letter-spacing: 1rpx;
}
.step-subtitle {
font-size: 28rpx;
color: #666;
margin-bottom: 60rpx;
line-height: 42rpx;
}
/* 类别列表 */
.category-list {
background: white;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
}
.category-item {
display: flex;
align-items: center;
padding: 36rpx;
border-bottom: 1rpx solid #f5f5f5;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.category-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0;
background: linear-gradient(90deg, rgba(255, 107, 0, 0.05), transparent);
transition: width 0.3s ease;
}
.category-item:active::before {
width: 100%;
}
.category-item:last-child {
border-bottom: none;
}
.category-item.selected {
background: #fff8f0;
border-left: 8rpx solid #FF6B00;
}
.category-icon {
font-size: 64rpx;
margin-right: 24rpx;
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.1));
}
.category-info {
flex: 1;
}
.category-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.rank-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
background: #F5F5F5;
color: #666;
border-radius: 6rpx;
font-size: 26rpx;
font-weight: 600;
margin-right: 20rpx;
min-width: 40rpx;
line-height: 40rpx;
text-align: center;
transition: all 0.2s ease;
}
/* 前三名的特殊样式 */
.rank-number.rank-1 {
background: #FF6B00;
color: white;
transform: scale(1.05);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.25);
}
.rank-number.rank-2 {
background: #FF9500;
color: white;
box-shadow: 0 4rpx 12rpx rgba(255, 149, 0, 0.2);
}
.rank-number.rank-3 {
background: #FFB74D;
color: white;
box-shadow: 0 4rpx 12rpx rgba(255, 183, 77, 0.15);
}
/* 销售标签样式 */
.sales-tag {
display: flex;
align-items: center;
background: linear-gradient(135deg, #FF6B00, #FF9500);
padding: 6rpx 16rpx;
border-radius: 20rpx;
margin-left: 10rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.2);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.sales-icon {
font-size: 22rpx;
margin-right: 6rpx;
}
.sales-count {
font-size: 22rpx;
color: #FFF;
font-weight: 600;
}
.category-desc {
font-size: 24rpx;
color: #666;
}
.category-arrow {
font-size: 36rpx;
color: #999;
}
/* 选项列表 */
.option-list {
background: white;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
}
.option-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 36rpx;
border-bottom: 1rpx solid #f5f5f5;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.option-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0;
background: linear-gradient(90deg, rgba(255, 107, 0, 0.05), transparent);
transition: width 0.3s ease;
}
.option-item:active::before {
width: 100%;
}
.option-item:last-child {
border-bottom: none;
}
.option-item.selected {
background: #fff8f0;
border-left: 8rpx solid #FF6B00;
}
.option-text {
font-size: 32rpx;
color: #333;
font-weight: 500;
display: flex;
align-items: center;
flex-wrap: wrap;
flex: 1;
}
/* 销售信息样式 */
.sales-info {
display: flex;
align-items: center;
background: rgba(255, 107, 0, 0.1);
padding: 6rpx 16rpx;
border-radius: 20rpx;
margin-left: 15rpx;
transition: all 0.2s ease;
}
.sales-info .sales-icon {
font-size: 22rpx;
margin-right: 6rpx;
}
.sales-info .sales-count {
font-size: 22rpx;
color: #FF6B00;
font-weight: 600;
}
.option-arrow {
font-size: 36rpx;
color: #999;
transition: transform 0.2s ease;
}
.option-item:active .option-arrow {
transform: translateX(4rpx);
}
/* 条件列表 */
.condition-list {
background: white;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
}
.condition-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 36rpx;
border-bottom: 1rpx solid #f5f5f5;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
text-align: left;
}
.condition-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0;
background: linear-gradient(90deg, rgba(255, 107, 0, 0.05), transparent);
transition: width 0.3s ease;
}
.condition-item:active::before {
width: 100%;
}
.condition-item:last-child {
border-bottom: none;
}
.condition-item.selected {
background: #fff8f0;
border-left: 8rpx solid #FF6B00;
}
.condition-info {
flex: 1;
text-align: left;
align-self: flex-start;
}
.condition-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
display: flex;
align-items: center;
text-align: left;
justify-content: flex-start;
align-self: flex-start;
}
.condition-desc {
font-size: 26rpx;
color: #666;
line-height: 36rpx;
text-align: left;
align-self: flex-start;
}
.condition-check {
color: #FF6B00;
font-size: 36rpx;
font-weight: bold;
background: white;
width: 50rpx;
height: 50rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.3);
}
/* 获取报价按钮 */
.get-price-section {
margin-top: 80rpx;
padding: 0 32rpx;
}
.get-price-btn {
background: linear-gradient(135deg, #FF6B00, #FF9500);
color: white;
border: none;
border-radius: 50rpx;
padding: 32rpx 0;
font-size: 34rpx;
font-weight: 700;
width: 100%;
box-shadow: 0 10rpx 30rpx rgba(255, 107, 0, 0.35);
transition: all 0.3s ease;
letter-spacing: 2rpx;
}
.get-price-btn:active {
transform: scale(0.98);
box-shadow: 0 6rpx 20rpx rgba(255, 107, 0, 0.3);
}
.get-price-btn::after {
content: none;
}
/* 结果页面 */
.result-step {
background: linear-gradient(135deg, #fff8f0, #ffffff);
min-height: 100vh;
}
.result-header {
text-align: center;
padding: 80rpx 0 30rpx;
background: white;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.03);
animation: fadeIn 0.8s ease-out;
}
.result-icon {
font-size: 140rpx;
margin-bottom: 30rpx;
animation: bounce 1s ease-out;
}
@keyframes bounce {
0% {
transform: translateY(-30rpx) scale(0.8);
opacity: 0;
}
70% {
transform: translateY(10rpx) scale(1.1);
}
100% {
transform: translateY(0) scale(1);
opacity: 1;
}
}
.result-title {
font-size: 48rpx;
font-weight: 700;
color: #333;
margin-bottom: 20rpx;
letter-spacing: 2rpx;
animation: fadeUp 0.6s ease-out;
}
.result-subtitle {
font-size: 28rpx;
color: #666;
animation: fadeUp 0.6s ease-out 0.2s both;
}
@keyframes fadeUp {
from {
transform: translateY(20rpx);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.result-content {
padding: 40rpx 32rpx;
}
/* 商品信息卡片 */
.product-info-card {
background: white;
border-radius: 20rpx;
padding: 40rpx;
margin-bottom: 50rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
animation: slideUp 0.8s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(40rpx);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.product-type {
font-size: 36rpx;
font-weight: 700;
color: #333;
margin-bottom: 20rpx;
letter-spacing: 1rpx;
}
.product-details {
display: flex;
gap: 20rpx;
flex-wrap: wrap;
}
.product-brand, .product-model {
font-size: 28rpx;
color: #666;
background: #f5f5f5;
padding: 14rpx 28rpx;
border-radius: 30rpx;
font-weight: 500;
transition: all 0.2s ease;
}
.product-brand:active, .product-model:active {
background: #e8e8e8;
}
/* 价格结果 */
.price-result {
text-align: center;
margin-top: 30rpx;
margin-bottom: 0;
padding: 0 32rpx;
animation: pulseScale 1s ease-out;
}
@keyframes pulseScale {
0% {
transform: scale(0.9);
opacity: 0;
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.price-label {
font-size: 28rpx;
color: #666;
margin-bottom: 24rpx;
font-weight: 500;
}
.price-amount {
display: flex;
align-items: baseline;
justify-content: center;
gap: 12rpx;
margin-bottom: 16rpx;
}
.price-symbol {
font-size: 44rpx;
color: #FF6B00;
font-weight: 700;
}
.price-number {
font-size: 80rpx;
font-weight: bold;
color: #FF6B00;
line-height: 1;
text-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.2);
}
.price-unit {
font-size: 30rpx;
color: #666;
font-weight: 500;
}
/* 状况汇总 */
.condition-summary {
background: white;
border-radius: 20rpx;
padding: 40rpx;
margin-bottom: 40rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
animation: slideUp 0.8s ease-out 0.4s both;
}
.summary-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
margin-bottom: 24rpx;
letter-spacing: 1rpx;
}
.condition-items {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.condition-item {
background: #f5f5f5;
padding: 20rpx 32rpx;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
flex: 1;
min-width: 200rpx;
transition: all 0.2s ease;
}
.condition-item:active {
background: #e8e8e8;
}
.condition-label {
font-size: 26rpx;
color: #666;
font-weight: 500;
}
.condition-value {
font-size: 32rpx;
font-weight: 700;
color: #333;
letter-spacing: 1rpx;
}
/* 价格提示 */
.price-tips {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 24rpx;
background: rgba(255, 107, 0, 0.05);
border-radius: 16rpx;
margin: 0 20rpx 60rpx;
animation: fadeUp 0.8s ease-out 0.6s both;
}
.tip-icon {
font-size: 30rpx;
color: #FF6B00;
flex-shrink: 0;
margin-top: 4rpx;
}
.tip-text {
font-size: 26rpx;
color: #666;
line-height: 38rpx;
flex: 1;
text-align: center;
}
/* 结果操作按钮 */
.result-actions {
display: flex;
gap: 24rpx;
padding: 0 32rpx 60rpx;
animation: slideUp 0.8s ease-out 0.8s both;
}
.btn-secondary {
flex: 1;
background: white;
color: #666;
border: 1rpx solid #e0e0e0;
border-radius: 50rpx;
padding: 32rpx 0;
font-size: 34rpx;
font-weight: 700;
transition: all 0.3s ease;
letter-spacing: 2rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
}
.btn-secondary:active {
background: #f8f8f8;
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
}
.btn-primary {
flex: 1;
background: linear-gradient(135deg, #FF6B00, #FF9500);
color: white;
border: none;
border-radius: 50rpx;
padding: 32rpx 0;
font-size: 34rpx;
font-weight: 700;
transition: all 0.3s ease;
letter-spacing: 2rpx;
box-shadow: 0 10rpx 30rpx rgba(255, 107, 0, 0.35);
}
.btn-primary:active {
transform: scale(0.98);
box-shadow: 0 6rpx 20rpx rgba(255, 107, 0, 0.3);
}
.btn-secondary::after, .btn-primary::after {
content: none;
}

977
pages/index/index.js

@ -0,0 +1,977 @@
// pages/index/index.js
const API = require('../../utils/api.js');
Page({
data: {
currentUserType: null,
showUserInfoForm: false,
avatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
nickname: '',
showAuthModal: false,
showOneKeyLoginModal: false,
// 测试模式开关,用于在未完成微信认证时进行测试
testMode: true
},
onLoad() {
console.log('首页初始化')
},
onShow: function () {
// 页面显示
// 更新自定义tabBar状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 0
});
}
// 更新全局tab状态
const app = getApp();
app.updateCurrentTab('index');
},
// 选择买家身份
async chooseBuyer() {
// 买家不需要登录验证,直接跳转到买家页面
this.finishSetUserType('buyer');
},
// 选择卖家身份
async chooseSeller() {
// 先检查是否登录
this.checkLoginAndProceed('seller');
},
// 检查登录状态并继续操作
checkLoginAndProceed(type) {
// 检查本地存储的登录信息
const openid = wx.getStorageSync('openid');
const userId = wx.getStorageSync('userId');
const userInfo = wx.getStorageSync('userInfo');
if (openid && userId && userInfo) {
console.log('用户已登录,直接处理身份选择');
// 用户已登录,直接处理
if (type === 'buyer') {
this.finishSetUserType(type);
} else if (type === 'seller') {
this.handleSellerRoute();
}
} else {
console.log('用户未登录,显示登录弹窗');
// 用户未登录,显示一键登录弹窗
this.setData({
pendingUserType: type,
showOneKeyLoginModal: true
});
}
},
// 处理卖家路由逻辑
async handleSellerRoute() {
try {
// 查询用户信息获取partnerstatus字段
const userInfo = await API.getUserInfo();
if (userInfo && userInfo.data && userInfo.data.partnerstatus) {
// 将partnerstatus存储到本地
wx.setStorageSync('partnerstatus', userInfo.data.partnerstatus);
console.log('获取到的partnerstatus:', userInfo.data.partnerstatus);
// 根据partnerstatus值控制路由跳转
if (userInfo.data.partnerstatus === 'approved') {
// 如果为approved,则进入seller/index页面
wx.switchTab({ url: '/pages/seller/index' });
} else {
// 否则进入pages/settlement/index页面
wx.navigateTo({ url: '/pages/settlement/index' });
}
} else {
// 如果没有获取到partnerstatus,默认进入settlement页面
console.log('未获取到partnerstatus字段');
wx.navigateTo({ url: '/pages/settlement/index' });
}
} catch (error) {
console.error('获取用户信息失败:', error);
// 出错时也进入settlement页面
wx.navigateTo({ url: '/pages/settlement/index' });
}
},
// 跳转到估价页面
// 显示一键登录弹窗
showOneKeyLogin() {
this.setData({
showAuthModal: false,
showOneKeyLoginModal: true
})
},
// 关闭未授权提示弹窗
closeAuthModal() {
this.setData({ showAuthModal: false })
},
// 关闭一键登录弹窗
closeOneKeyLoginModal() {
this.setData({ showOneKeyLoginModal: false })
},
// 处理手机号授权
async onGetPhoneNumber(e) {
// 打印详细错误信息,方便调试
console.log('getPhoneNumber响应:', e.detail)
// 关闭手机号授权弹窗
this.setData({ showOneKeyLoginModal: false })
// 用户点击拒绝授权
if (e.detail.errMsg === 'getPhoneNumber:fail user deny') {
wx.showToast({
title: '需要授权手机号才能使用',
icon: 'none',
duration: 2000
})
return
}
// 处理没有权限的情况
if (e.detail.errMsg === 'getPhoneNumber:fail no permission') {
// 如果是测试模式,跳过真实授权流程
if (this.data.testMode) {
console.log('进入测试模式,跳过真实手机号授权')
await this.simulateLoginForTest()
return
}
wx.showToast({
title: '当前环境无法获取手机号权限',
icon: 'none',
duration: 3000
})
// 增加关于微信认证要求的说明
console.warn('获取手机号权限失败: 请注意,微信小程序获取手机号功能需要满足以下条件:1. 小程序必须完成微信企业认证;2. 需要在小程序后台配置相应权限;3. 必须使用button组件的open-type="getPhoneNumber"触发。')
return
}
// 检查是否已经登录,避免重复授权
const existingOpenid = wx.getStorageSync('openid')
const existingUserId = wx.getStorageSync('userId')
const existingUserInfo = wx.getStorageSync('userInfo')
if (existingOpenid && existingUserId && existingUserInfo && existingUserInfo.phoneNumber !== '13800138000') {
console.log('用户已登录且手机号有效,直接完成身份设置')
// 直接完成身份设置,跳过重复授权
const currentUserType = this.data.pendingUserType || this.data.currentUserType || 'buyer'
this.finishSetUserType(currentUserType)
return
}
wx.showLoading({
title: '登录中...',
mask: true
})
// 引入API服务
const API = require('../../utils/api.js')
try {
if (e.detail.errMsg === 'getPhoneNumber:ok') {
// 用户同意授权,实际处理授权流程
console.log('用户同意授权获取手机号')
// 1. 先执行微信登录获取code
const loginRes = await new Promise((resolve, reject) => {
wx.login({
success: resolve,
fail: reject
})
})
if (!loginRes.code) {
throw new Error('获取登录code失败')
}
console.log('获取登录code成功:', loginRes.code)
// 2. 使用code换取openid
const openidRes = await API.getOpenid(loginRes.code)
// 改进错误处理逻辑,更宽容地处理服务器返回格式,增加详细日志
let openid = null;
let userId = null;
console.log('openidRes完整响应:', JSON.stringify(openidRes));
if (openidRes && typeof openidRes === 'object') {
// 适配服务器返回格式:{success: true, code: 200, message: '获取openid成功', data: {openid, userId}}
if (openidRes.data && typeof openidRes.data === 'object') {
console.log('识别到标准服务器返回格式,从data字段提取信息');
openid = openidRes.data.openid || openidRes.data.OpenID || null;
userId = openidRes.data.userId || null;
} else {
// 尝试从响应对象中直接提取openid,适配其他可能的格式
console.log('尝试从根对象直接提取openid');
openid = openidRes.openid || openidRes.OpenID || null;
userId = openidRes.userId || null;
}
}
if (!openid) {
console.error('无法从服务器响应中提取openid,完整响应:', JSON.stringify(openidRes));
// 增加更友好的错误信息,指导用户检查服务器配置
throw new Error(`获取openid失败: 服务器返回数据格式可能不符合预期,请检查服务器配置。响应数据为: ${JSON.stringify(openidRes)}`);
}
console.log('获取openid成功:', openid)
// 3. 存储openid和session_key
wx.setStorageSync('openid', openid)
// 从服务器返回中获取session_key
if (openidRes && openidRes.session_key) {
wx.setStorageSync('sessionKey', openidRes.session_key)
} else if (openidRes && openidRes.data && openidRes.data.session_key) {
wx.setStorageSync('sessionKey', openidRes.data.session_key)
}
// 优先使用从服务器响应data字段中提取的userId
if (userId) {
wx.setStorageSync('userId', userId)
console.log('使用从服务器data字段提取的userId:', userId)
} else if (openidRes && openidRes.userId) {
wx.setStorageSync('userId', openidRes.userId)
console.log('使用服务器根对象中的userId:', openidRes.userId)
} else {
// 生成临时userId
const tempUserId = 'user_' + Date.now()
wx.setStorageSync('userId', tempUserId)
console.log('生成临时userId:', tempUserId)
}
// 4. 上传手机号加密数据到服务器解密
const phoneData = {
...e.detail,
openid: openid
}
console.log('准备上传手机号加密数据到服务器')
const phoneRes = await API.uploadPhoneNumberData(phoneData)
// 改进手机号解密结果的处理逻辑
if (!phoneRes || (!phoneRes.success && !phoneRes.phoneNumber)) {
// 如果服务器返回格式不标准但包含手机号,也接受
if (phoneRes && phoneRes.phoneNumber) {
console.warn('服务器返回格式可能不符合预期,但成功获取手机号');
} else {
throw new Error('获取手机号失败: ' + (phoneRes && phoneRes.message ? phoneRes.message : '未知错误'))
}
}
// 检查是否有手机号冲突
const hasPhoneConflict = phoneRes.phoneNumberConflict || false
const isNewPhone = phoneRes.isNewPhone || true
const phoneNumber = phoneRes.phoneNumber || null
// 如果有手机号冲突且没有返回手机号,使用临时手机号
const finalPhoneNumber = hasPhoneConflict && !phoneNumber ? '13800138000' : phoneNumber
console.log('手机号解密结果:', {
phoneNumber: finalPhoneNumber,
hasPhoneConflict: hasPhoneConflict,
isNewPhone: isNewPhone
})
// 5. 获取用户微信名称和头像
let userProfile = null;
try {
userProfile = await new Promise((resolve, reject) => {
wx.getUserProfile({
desc: '用于完善会员资料',
success: resolve,
fail: reject
});
});
console.log('获取用户信息成功:', userProfile);
} catch (err) {
console.warn('获取用户信息失败:', err);
// 如果获取失败,使用默认值
}
// 6. 创建用户信息
const tempUserInfo = {
nickName: userProfile ? userProfile.userInfo.nickName : '微信用户',
avatarUrl: userProfile ? userProfile.userInfo.avatarUrl : this.data.avatarUrl,
gender: userProfile ? userProfile.userInfo.gender : 0,
country: userProfile ? userProfile.userInfo.country : '',
province: userProfile ? userProfile.userInfo.province : '',
city: userProfile ? userProfile.userInfo.city : '',
language: userProfile ? userProfile.userInfo.language : 'zh_CN',
phoneNumber: finalPhoneNumber
}
// 从本地存储获取userId(使用已声明的变量)
const storedUserId = wx.getStorageSync('userId')
// 优先使用用户之前选择的身份类型,如果没有则尝试获取已存储的或默认为买家
const users = wx.getStorageSync('users') || {}
const currentUserType = this.data.pendingUserType || this.data.currentUserType ||
(users[storedUserId] && users[storedUserId].type ? users[storedUserId].type : 'buyer')
console.log('用户身份类型:', currentUserType)
// 清除临时存储的身份类型
if (this.data.pendingUserType) {
this.setData({ pendingUserType: null })
}
// 保存用户信息并等待上传完成
console.log('开始保存用户信息并上传到服务器...')
const uploadResult = await this.saveUserInfo(tempUserInfo, currentUserType)
console.log('用户信息保存并上传完成')
wx.hideLoading()
// 根据服务器返回的结果显示不同的提示
if (uploadResult && uploadResult.phoneNumberConflict) {
wx.showToast({
title: '登录成功,但手机号已被其他账号绑定',
icon: 'none',
duration: 3000
})
} else {
wx.showToast({
title: '登录成功,手机号已绑定',
icon: 'success',
duration: 2000
})
}
// 完成设置并跳转
this.finishSetUserType(currentUserType)
} else {
// 用户拒绝授权或其他情况
console.log('手机号授权失败:', e.detail.errMsg)
// 不再抛出错误,而是显示友好的提示
wx.hideLoading()
wx.showToast({
title: '需要授权手机号才能使用',
icon: 'none',
duration: 2000
})
return
}
} catch (error) {
wx.hideLoading()
console.error('登录过程中发生错误:', error)
// 更具体的错误提示
let errorMsg = '登录失败,请重试'
if (error.message.includes('网络')) {
errorMsg = '网络连接失败,请检查网络后重试'
} else if (error.message.includes('服务器')) {
errorMsg = '服务器连接失败,请稍后重试'
}
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
})
// 清除可能已经保存的不完整信息
try {
wx.removeStorageSync('openid')
wx.removeStorageSync('sessionKey')
wx.removeStorageSync('userId')
} catch (e) {
console.error('清除临时登录信息失败:', e)
}
}
},
// 处理用户基本信息授权
handleUserAuth(type) {
// 保存当前用户类型
this.setData({
currentUserType: type
})
// 先执行微信登录
this.doWechatLogin(type)
},
// 测试模式下模拟登录流程
async simulateLoginForTest() {
wx.showLoading({
title: '测试模式登录中...',
mask: true
})
try {
// 1. 模拟微信登录,生成测试用的code
const mockCode = 'test_code_' + Date.now()
console.log('模拟获取登录code:', mockCode)
// 2. 模拟获取openid和userId
const mockOpenid = 'test_openid_' + Date.now()
const mockUserId = 'test_user_' + Date.now()
console.log('模拟获取openid:', mockOpenid)
console.log('模拟获取userId:', mockUserId)
// 3. 存储测试数据
wx.setStorageSync('openid', mockOpenid)
wx.setStorageSync('userId', mockUserId)
// 4. 模拟手机号解密结果
const mockPhoneNumber = '13800138000'
console.log('模拟手机号解密成功:', mockPhoneNumber)
// 5. 创建模拟用户信息
const mockUserInfo = {
nickName: '测试用户',
avatarUrl: this.data.avatarUrl,
gender: 0,
country: '测试国家',
province: '测试省份',
city: '测试城市',
language: 'zh_CN',
phoneNumber: mockPhoneNumber
}
// 6. 获取用户身份类型(优先使用pendingUserType)
const userId = wx.getStorageSync('userId')
const users = wx.getStorageSync('users') || {}
const currentUserType = this.data.pendingUserType || this.data.currentUserType ||
(users[userId] && users[userId].type ? users[userId].type : 'buyer')
console.log('测试模式用户身份类型:', currentUserType)
// 7. 清除临时存储的身份类型
if (this.data.pendingUserType) {
this.setData({ pendingUserType: null })
}
// 8. 保存用户信息并等待上传完成
console.log('测试模式开始保存用户信息...')
// 在测试模式下也会上传用户信息到服务器,用于连通性测试
await this.saveUserInfo(mockUserInfo, currentUserType)
console.log('测试模式用户信息保存完成')
wx.hideLoading()
// 9. 显示成功提示
wx.showToast({
title: '测试模式登录成功',
icon: 'success',
duration: 2000
})
// 10. 完成设置并跳转
this.finishSetUserType(currentUserType)
} catch (error) {
wx.hideLoading()
console.error('测试模式登录过程中发生错误:', error)
wx.showToast({
title: '测试模式登录失败',
icon: 'none',
duration: 2000
})
}
},
// 执行微信登录并获取openid
async doWechatLogin(type) {
// 显示加载提示
wx.showLoading({
title: '登录中...',
mask: true
})
try {
// 调用微信登录接口
const loginRes = await new Promise((resolve, reject) => {
wx.login({
success: resolve,
fail: reject
})
})
if (loginRes.code) {
console.log('微信登录成功,code:', loginRes.code)
// 保存登录凭证
try {
wx.setStorageSync('loginCode', loginRes.code)
} catch (e) {
console.error('保存登录凭证失败:', e)
}
// 引入API服务
const API = require('../../utils/api.js')
// 发送code和用户类型到服务器换取openid和session_key
try {
const openidRes = await API.getOpenid(loginRes.code, type)
console.log('获取openid响应:', openidRes)
// 增强版响应处理逻辑,支持多种返回格式
let openid = null;
let userId = null;
let sessionKey = null;
// 优先从data字段获取数据
if (openidRes && openidRes.data && typeof openidRes.data === 'object') {
openid = openidRes.data.openid || openidRes.data.OpenID || null;
userId = openidRes.data.userId || openidRes.data.userid || null;
sessionKey = openidRes.data.session_key || openidRes.data.sessionKey || null;
}
// 如果data为空或不存在,尝试从响应对象直接获取
if (!openid && openidRes && typeof openidRes === 'object') {
console.warn('服务器返回格式可能不符合预期,data字段为空或不存在,但尝试从根对象提取信息:', openidRes);
openid = openidRes.openid || openidRes.OpenID || null;
userId = openidRes.userId || openidRes.userid || null;
sessionKey = openidRes.session_key || openidRes.sessionKey || null;
}
// 检查服务器状态信息
const isSuccess = openidRes && (openidRes.success === true || openidRes.code === 200);
const serverMessage = openidRes && (openidRes.message || openidRes.msg);
if (isSuccess && !openid) {
console.warn('服务器返回成功状态,但未包含有效的openid:', openidRes);
}
// 打印获取到的信息,方便调试
console.log('提取到的登录信息:', { openid, userId, sessionKey, serverMessage });
if (openid) {
// 存储openid和session_key
wx.setStorageSync('openid', openid)
if (sessionKey) {
wx.setStorageSync('sessionKey', sessionKey)
}
// 如果有userId,也存储起来
if (userId) {
wx.setStorageSync('userId', userId)
}
console.log('获取openid成功并存储:', openid)
// 验证登录状态并获取用户信息
await this.validateLoginAndGetUserInfo(openid)
} else {
// 即使没有获取到openid,也要继续用户信息授权流程
console.warn('未获取到有效的openid,但继续用户信息授权流程:', openidRes);
// 设置一个临时的openid以便继续流程
wx.setStorageSync('openid', 'temp_' + Date.now())
}
} catch (error) {
console.error('获取openid失败:', error)
// 即使获取openid失败,也继续用户信息授权流程
}
// 继续用户信息授权流程,等待完成
await this.processUserInfoAuth(type)
} else {
wx.hideLoading()
console.error('微信登录失败:', loginRes)
wx.showToast({
title: '登录失败,请重试',
icon: 'none',
duration: 2000
})
}
} catch (err) {
wx.hideLoading()
console.error('wx.login失败:', err)
wx.showToast({
title: '获取登录状态失败',
icon: 'none',
duration: 2000
})
}
},
// 验证登录状态并获取用户信息
async validateLoginAndGetUserInfo(openid) {
try {
// 引入API服务
const API = require('../../utils/api.js')
// 调用服务器验证登录状态
const validateRes = await API.validateUserLogin()
if (validateRes.success && validateRes.userInfo) {
// 服务器返回了用户信息,同步到本地
const app = getApp()
const userInfo = validateRes.userInfo
// 更新全局用户信息
app.globalData.userInfo = userInfo
// 存储用户信息到本地
wx.setStorageSync('userInfo', userInfo)
console.log('验证登录状态成功,用户信息已同步:', userInfo)
// 检查是否为临时手机号,如果是则提示用户重新授权
if (userInfo.phoneNumber === '13800138000') {
console.warn('检测到临时手机号,建议用户重新授权')
// 设置重新授权标志
wx.setStorageSync('needPhoneAuth', true)
} else {
// 清除可能存在的重新授权标志
wx.removeStorageSync('needPhoneAuth')
console.log('手机号验证通过:', userInfo.phoneNumber)
}
return true
} else {
console.warn('服务器验证失败,可能是新用户或登录状态无效')
return false
}
} catch (error) {
console.error('验证登录状态失败:', error)
// 如果验证失败,清除可能存在的无效登录信息
try {
wx.removeStorageSync('openid')
wx.removeStorageSync('userId')
wx.removeStorageSync('userInfo')
} catch (e) {
console.error('清除无效登录信息失败:', e)
}
return false
}
},
// 处理用户信息授权
async processUserInfoAuth(type) {
const app = getApp()
// 如果已经有用户信息,直接完成设置并跳转
if (app.globalData.userInfo) {
wx.hideLoading()
this.finishSetUserType(type)
return
}
// 优化:首次登录时自动创建临时用户信息并完成登录,不再需要用户填写表单
// 获取已存储的userId或生成新的
let userId = wx.getStorageSync('userId')
if (!userId) {
userId = 'user_' + Date.now()
wx.setStorageSync('userId', userId)
}
// 创建临时用户信息
const tempUserInfo = {
nickName: '微信用户',
avatarUrl: this.data.avatarUrl,
gender: 0,
country: '',
province: '',
city: '',
language: 'zh_CN'
}
try {
// 保存临时用户信息并完成登录,等待数据上传完成
await this.saveUserInfo(tempUserInfo, type)
// 隐藏加载提示
wx.hideLoading()
// 数据上传完成后再跳转
this.finishSetUserType(type)
} catch (error) {
console.error('处理用户信息授权失败:', error)
wx.hideLoading()
wx.showToast({
title: '登录失败,请重试',
icon: 'none',
duration: 2000
})
}
},
// 保存用户信息
async saveUserInfo(userInfo, type) {
// 确保userId存在
let userId = wx.getStorageSync('userId')
if (!userId) {
userId = 'user_' + Date.now()
wx.setStorageSync('userId', userId)
}
// 保存用户信息到本地存储 - 修复首次获取问题
let users = wx.getStorageSync('users')
// 如果users不存在或不是对象,初始化为空对象
if (!users || typeof users !== 'object') {
users = {}
}
// 初始化用户信息
users[userId] = users[userId] || {}
users[userId].info = userInfo
users[userId].type = type
// 确保存储操作成功
try {
wx.setStorageSync('users', users)
console.log('用户信息已成功保存到本地存储')
} catch (e) {
console.error('保存用户信息到本地存储失败:', e)
}
// 保存用户信息到全局变量
const app = getApp()
app.globalData.userInfo = userInfo
app.globalData.userType = type
console.log('用户信息已保存到全局变量:', userInfo)
// 额外保存一份单独的userInfo到本地存储,便于checkPhoneAuthSetting方法检查
try {
wx.setStorageSync('userInfo', userInfo)
console.log('单独的userInfo已保存')
} catch (e) {
console.error('保存单独的userInfo失败:', e)
}
// 上传用户信息到服务器
// 在测试模式下也上传用户信息,用于连通性测试
console.log('准备上传用户信息到服务器进行测试...')
// 确保测试数据包含服务器所需的所有字段
const completeUserInfo = {
...userInfo,
// 确保包含服务器需要的必要字段
nickName: userInfo.nickName || '测试用户',
phoneNumber: userInfo.phoneNumber || '13800138000'
}
try {
const uploadResult = await this.uploadUserInfoToServer(completeUserInfo, userId, type)
console.log('用户信息上传到服务器成功')
return uploadResult // 返回上传结果
} catch (error) {
console.error('用户信息上传到服务器失败:', error)
// 显示友好的提示,但不中断流程
wx.showToast({
title: '测试数据上传失败,不影响使用',
icon: 'none',
duration: 2000
})
// 不再抛出错误,而是返回默认成功结果,确保登录流程继续
return {
success: true,
message: '本地登录成功,服务器连接失败'
}
}
},
// 处理头像选择
onChooseAvatar(e) {
const { avatarUrl } = e.detail
this.setData({
avatarUrl
})
},
// 处理昵称提交
getUserName(e) {
const { nickname } = e.detail.value
const type = this.data.currentUserType
if (!nickname) {
wx.showToast({
title: '请输入昵称',
icon: 'none',
duration: 2000
})
return
}
// 创建用户信息对象
const userInfo = {
nickName: nickname,
avatarUrl: this.data.avatarUrl,
// 其他可能需要的字段
gender: 0,
country: '',
province: '',
city: '',
language: 'zh_CN'
}
// 保存用户信息
this.saveUserInfo(userInfo, type)
// 隐藏表单
this.setData({
showUserInfoForm: false
})
// 完成设置并跳转
this.finishSetUserType(type)
},
// 取消用户信息表单
cancelUserInfoForm() {
this.setData({
showUserInfoForm: false
})
wx.hideLoading()
},
// 上传用户信息到服务器
async uploadUserInfoToServer(userInfo, userId, type) {
// 引入API服务
const API = require('../../utils/api.js')
// 获取openid
const openid = wx.getStorageSync('openid')
// 构造上传数据(包含openid和session_key)
const uploadData = {
userId: userId,
openid: openid,
...userInfo,
type: type,
timestamp: Date.now()
}
// 调用API上传用户信息并返回Promise
try {
const res = await API.uploadUserInfo(uploadData)
console.log('用户信息上传成功:', res)
return res
} catch (err) {
console.error('用户信息上传失败:', err)
// 不再抛出错误,而是返回默认成功结果,确保登录流程继续
// 这样即使服务器连接失败,本地登录也能完成
return {
success: true,
message: '本地登录成功,服务器连接失败'
}
}
},
// 处理手机号授权结果(已重命名为onPhoneNumberResult,此方法已废弃)
processPhoneAuthResult: function () {
console.warn('processPhoneAuthResult方法已废弃,请使用onPhoneNumberResult方法')
},
// 手机号授权处理
async onPhoneNumberResult(e) {
console.log('手机号授权结果:', e)
if (e.detail.errMsg === 'getPhoneNumber:ok') {
// 用户同意授权,获取加密数据
const phoneData = e.detail
wx.showLoading({ title: '获取手机号中...' })
try {
// 引入API服务
const API = require('../../utils/api.js')
// 上传到服务器解密
const res = await API.uploadPhoneNumberData(phoneData)
wx.hideLoading()
if (res.success && res.phoneNumber) {
console.log('获取手机号成功:', res.phoneNumber)
// 保存手机号到用户信息
const app = getApp()
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {}
userInfo.phoneNumber = res.phoneNumber
// 更新本地和全局用户信息
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
// 获取userId
const userId = wx.getStorageSync('userId')
const users = wx.getStorageSync('users') || {}
const currentUserType = users[userId] && users[userId].type ? users[userId].type : ''
// 同时更新服务器用户信息,确保上传完成
console.log('开始更新服务器用户信息...')
if (!this.data.testMode) {
await this.uploadUserInfoToServer(userInfo, userId, currentUserType)
console.log('服务器用户信息更新完成')
} else {
console.log('测试模式下跳过服务器用户信息更新')
}
wx.showToast({ title: '手机号绑定成功', icon: 'success' })
} else {
console.error('获取手机号失败:', res)
wx.showToast({ title: '获取手机号失败', icon: 'none' })
}
} catch (err) {
wx.hideLoading()
console.error('获取手机号失败:', err)
wx.showToast({ title: '获取手机号失败', icon: 'none' })
}
} else {
console.log('用户拒绝授权手机号')
}
},
// 完成用户类型设置并跳转
finishSetUserType(type) {
const userId = wx.getStorageSync('userId')
// 更新用户类型
let users = wx.getStorageSync('users')
// 检查users是否为对象,如果不是则重新初始化为空对象
if (typeof users !== 'object' || users === null) {
users = {}
}
// 确保userId对应的用户对象存在
if (!users[userId]) {
users[userId] = {}
}
users[userId].type = type
wx.setStorageSync('users', users)
// 打标签
let tags = wx.getStorageSync('tags')
// 检查tags是否为对象,如果不是则重新初始化为空对象
if (typeof tags !== 'object' || tags === null) {
tags = {}
}
// 确保userId对应的标签数组存在
tags[userId] = tags[userId] || []
// 移除已有的身份标签
tags[userId] = tags[userId].filter(tag => !tag.startsWith('身份:'))
// 添加新的身份标签
tags[userId].push(`身份:${type}`)
wx.setStorageSync('tags', tags)
console.log('用户类型设置完成,准备跳转到', type === 'buyer' ? '买家页面' : '卖家页面')
// 添加小延迟确保所有异步操作都完成后再跳转
setTimeout(() => {
// 跳转到对应页面
if (type === 'buyer') {
wx.switchTab({ url: '/pages/buyer/index' })
} else {
// 卖家身份需要处理partnerstatus逻辑,调用专门的方法
this.handleSellerRoute()
}
}, 500)
},
// 前往个人中心
toProfile() {
wx.switchTab({ url: '/pages/profile/index' })
}
})

5
pages/index/index.json

@ -0,0 +1,5 @@
{
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

94
pages/index/index.wxml

@ -0,0 +1,94 @@
<view class="container">
<image src="/images/生成鸡蛋贸易平台图片.png" style="width: 100%; height: 425rpx; margin: -280rpx auto 20rpx; display: block;"></image>
<view class="title" style="margin-top: 60rpx;">中国最专业的鸡蛋现货交易平台</view>
<view class="desc" style="margin: 30rpx 0; text-align: center; padding: 0 20rpx;">
请选择您的需求,我们将为您提供专属服务
</view>
<!-- 身份选择 -->
<view style="text-align: center; margin-top: 30rpx; margin-bottom: 30rpx;">
<text style="font-size: 28rpx; color: #666; display: block; margin-bottom: 15rpx;"></text>
<button class="btn buyer-btn" bindtap="chooseBuyer">
我要买蛋
</button>
<button class="btn seller-btn" bindtap="chooseSeller">
我要卖蛋
</button>
</view>
<!-- 未授权登录提示弹窗 -->
<view wx:if="{{showAuthModal}}" class="modal-overlay">
<view class="modal-container">
<view class="modal-title">
<text>提示</text>
</view>
<view class="modal-content">
<text>您还没有授权登录</text>
</view>
<view class="modal-buttons">
<button class="primary-button" bindtap="showOneKeyLogin">
一键登录
</button>
<button class="cancel-button" bindtap="closeAuthModal">取消</button>
</view>
</view>
</view>
<!-- 一键登录弹窗 -->
<view wx:if="{{showOneKeyLoginModal}}" class="modal-overlay">
<view class="modal-container">
<view class="modal-title">
<text>授权登录</text>
</view>
<view class="modal-content">
<text>请授权获取您的手机号用于登录</text>
</view>
<view class="modal-buttons">
<button class="primary-button" open-type="getPhoneNumber" bind:getphonenumber="onGetPhoneNumber">
授权获取手机号
</button>
<button class="cancel-button" bindtap="closeOneKeyLoginModal">取消</button>
</view>
</view>
</view>
<!-- 用户信息填写弹窗 -->
<view wx:if="{{showUserInfoForm}}" class="modal-overlay">
<view class="modal-container">
<view class="modal-title">
<text>完善个人信息</text>
</view>
<!-- 头像选择 -->
<view class="avatar-section">
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image class="avatar" src="{{avatarUrl}}"></image>
</button>
</view>
<!-- 昵称输入 -->
<form bindsubmit="getUserName">
<view class="form-group">
<view class="form-label">昵称</view>
<input placeholder="请输入昵称" type="nickname" name="nickname" maxlength="32" class="form-input"></input>
</view>
<!-- 提交按钮 -->
<view class="form-actions">
<button form-type="submit" class="confirm-button">确定</button>
</view>
</form>
<!-- 取消按钮 -->
<view class="modal-buttons">
<button class="cancel-button" bindtap="cancelUserInfoForm">取消</button>
</view>
</view>
</view>
<view class="login-hint">
已有账号?<text class="link-text" bindtap="toProfile">进入我的页面</text>
</view>
</view>

243
pages/index/index.wxss

@ -0,0 +1,243 @@
/**index.wxss**/
page {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
margin: 0;
padding: 0;
}
.container {
padding: 0;
margin: 0;
width: 100%;
}
.scrollarea {
flex: 1;
overflow-y: hidden;
}
/* 玻璃质感按钮样式 */
.btn {
/* 基础样式重置 */
border: none;
border-radius: 24rpx;
font-size: 32rpx;
font-weight: 600;
padding: 28rpx 0;
margin: 0 auto 24rpx;
width: 80%;
display: block;
text-align: center;
white-space: nowrap;
line-height: 1.5;
/* 玻璃质感效果 */
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(12rpx);
-webkit-backdrop-filter: blur(12rpx);
border: 1rpx solid rgba(255, 255, 255, 0.3);
box-shadow:
0 8rpx 32rpx rgba(31, 38, 135, 0.2),
0 4rpx 16rpx rgba(0, 0, 0, 0.1),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.7),
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.1);
/* 过渡效果 */
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
/* 买家按钮样式 */
.buyer-btn {
color: #07c160;
background: rgba(7, 193, 96, 0.15);
}
/* 卖家按钮样式 */
.seller-btn {
color: #1677ff;
background: rgba(22, 119, 255, 0.15);
}
/* 估价按钮样式 */
.evaluate-btn {
color: #4CAF50;
background: rgba(76, 175, 80, 0.15);
}
/* 立即入驻按钮样式 */
.settlement-btn {
color: #2196F3;
background: rgba(33, 150, 243, 0.15);
}
/* 按钮点击效果 */
.btn:active {
transform: scale(0.98);
box-shadow:
0 4rpx 16rpx rgba(31, 38, 135, 0.1),
0 2rpx 8rpx rgba(0, 0, 0, 0.05),
inset 0 1rpx 2rpx rgba(255, 255, 255, 0.5),
inset 0 -1rpx 2rpx rgba(0, 0, 0, 0.05);
}
/* 按钮悬浮光晕效果 */
.btn::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transform: rotate(45deg);
animation: shine 3s infinite;
opacity: 0;
}
@keyframes shine {
0% { transform: translateX(-100%) rotate(45deg); opacity: 0; }
50% { opacity: 0.2; }
100% { transform: translateX(100%) rotate(45deg); opacity: 0; }
}
.btn:active::after {
animation: none;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.modal-container {
background-color: white;
border-radius: 16rpx;
width: 80%;
padding: 40rpx;
}
.modal-title {
text-align: center;
margin-bottom: 30rpx;
}
.modal-title text {
font-size: 36rpx;
font-weight: bold;
}
.modal-content {
text-align: center;
margin-bottom: 40rpx;
color: #666;
}
.modal-content text {
font-size: 32rpx;
}
.modal-buttons {
text-align: center;
}
.primary-button {
background-color: #1677ff;
color: white;
width: 100%;
border-radius: 8rpx;
margin-bottom: 20rpx;
border: none;
}
.cancel-button {
background: none;
color: #666;
border: none;
}
/* 头像选择样式 */
.avatar-section {
text-align: center;
margin-bottom: 40rpx;
}
.avatar-wrapper {
padding: 0;
background: none;
border: none;
}
.avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
}
/* 表单样式 */
.form-group {
margin-bottom: 30rpx;
}
.form-label {
font-size: 28rpx;
margin-bottom: 10rpx;
display: block;
}
.form-input {
border: 1rpx solid #eee;
border-radius: 8rpx;
padding: 20rpx;
width: 100%;
max-width: 100%;
box-sizing: border-box;
font-size: 28rpx;
}
.form-actions {
text-align: center;
margin-top: 40rpx;
}
.confirm-button {
background-color: #07c160;
color: white;
width: 100%;
border-radius: 8rpx;
border: none;
}
/* 登录提示样式 */
.login-hint {
margin-top: 50rpx;
font-size: 28rpx;
color: #666;
text-align: center;
}
.link-text {
color: #1677ff;
}
/* 标题样式 */
.title {
font-size: 40rpx;
font-weight: bold;
text-align: center;
color: #333;
margin-top: 20rpx;
margin-bottom: 10rpx;
}

64
pages/notopen/index.js

@ -0,0 +1,64 @@
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
},
/**
* 返回首页
*/
onBackTap: function () {
wx.switchTab({
url: '/pages/index/index'
})
}
})

4
pages/notopen/index.json

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "又鸟蛋平台",
"usingComponents": {}
}

8
pages/notopen/index.wxml

@ -0,0 +1,8 @@
<view class="container">
<view class="icon">
<view class="lock-icon"></view>
</view>
<view class="title">功能暂未开放</view>
<view class="subtitle">该功能正在紧张开发中,敬请期待</view>
<button class="back-btn" bindtap="onBackTap">返回首页</button>
</view>

69
pages/notopen/index.wxss

@ -0,0 +1,69 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
background-color: #ffffff;
padding: 40rpx;
box-sizing: border-box;
margin: 0;
}
/* 重置页面默认样式 */
page {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
background-color: #ffffff;
}
.icon {
margin-bottom: 60rpx;
}
.lock-icon {
width: 120rpx;
height: 120rpx;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23FFD93D"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>');
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
margin-bottom: 24rpx;
}
.subtitle {
font-size: 28rpx;
color: #999999;
margin-bottom: 80rpx;
text-align: center;
}
.back-btn {
width: 320rpx;
height: 88rpx;
background-color: #1989fa;
color: white;
font-size: 32rpx;
border-radius: 44rpx;
box-shadow: 0 4rpx 16rpx rgba(25, 137, 250, 0.3);
border: none;
outline: none;
}
.back-btn::after {
border: none;
}
.back-btn:active {
background-color: #0c7ad9;
transform: scale(0.98);
}

652
pages/profile/index.js

@ -0,0 +1,652 @@
// pages/profile/index.js
Page({
data: {
userInfo: {},
userType: '',
userTags: [],
needPhoneAuth: false // 是否需要重新授权手机号
},
onLoad() {
this.loadUserInfo()
},
onShow() {
this.loadUserInfo()
// 更新自定义tabBar状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 3
});
}
// 更新全局tab状态
const app = getApp();
app.updateCurrentTab('profile');
},
// 加载用户信息
loadUserInfo() {
console.log('开始加载用户信息')
const app = getApp()
// 从本地存储获取用户信息
const localUserInfo = wx.getStorageSync('userInfo') || {}
if (app.globalData.userInfo) {
this.setData({ userInfo: app.globalData.userInfo })
} else {
app.globalData.userInfo = localUserInfo
this.setData({
userInfo: localUserInfo,
needPhoneAuth: !localUserInfo.phoneNumber || localUserInfo.phoneNumber === '13800138000'
})
}
// 加载用户类型和标签
const userId = wx.getStorageSync('userId')
const openid = wx.getStorageSync('openid')
console.log('加载用户信息 - userId:', userId, 'openid:', openid ? '已获取' : '未获取')
if (userId && openid) {
// 从服务器获取最新的用户信息,确保手机号是最新的
this.refreshUserInfoFromServer(openid, userId)
// 确保users存储结构存在
let users = wx.getStorageSync('users')
if (!users) {
users = {}
wx.setStorageSync('users', users)
}
if (!users[userId]) {
users[userId] = { type: '' }
wx.setStorageSync('users', users)
}
const user = users[userId]
const currentType = this.formatUserType(user.type)
this.setData({ userType: currentType })
console.log('加载用户信息 - 当前用户类型:', currentType)
// 确保tags存储结构存在
let tags = wx.getStorageSync('tags')
if (!tags) {
tags = {}
wx.setStorageSync('tags', tags)
}
if (!tags[userId]) {
tags[userId] = []
wx.setStorageSync('tags', tags)
}
const userTags = tags[userId] || []
console.log('加载用户信息 - 原始标签:', userTags)
// 使用indexOf替代includes以解决Babel兼容性问题
let firstCategoryTag = []
let identityTags = []
// 查找第一个偏好品类标签
for (let i = 0; i < userTags.length; i++) {
if (userTags[i].indexOf('偏好品类') !== -1) {
firstCategoryTag = [userTags[i]]
break
}
}
// 合并保留的标签
let filteredTags = [...firstCategoryTag]
// 始终根据当前用户类型显示对应的身份标签,而不是使用存储的标签
if (user.type && user.type !== '') {
let identityLabel = '身份:not_set'
switch (user.type) {
case 'buyer': identityLabel = '身份:buyer'; break
case 'seller': identityLabel = '身份:seller'; break
case 'both': identityLabel = '身份:buyer+seller'; break
}
filteredTags.push(identityLabel)
console.log('加载用户信息 - 根据当前用户类型显示身份标签:', identityLabel)
} else {
// 如果没有用户类型,但有存储的身份标签,显示第一个
for (let i = 0; i < userTags.length; i++) {
if (userTags[i].indexOf('身份') !== -1) {
filteredTags.push(userTags[i])
console.log('加载用户信息 - 显示存储的身份标签:', userTags[i])
break
}
}
}
console.log('加载用户信息 - 过滤后的标签:', filteredTags)
this.setData({ userTags: filteredTags })
}
},
// 从服务器刷新用户信息
refreshUserInfoFromServer(openid, userId) {
const API = require('../../utils/api.js')
API.getUserInfo(openid).then(res => {
console.log('从服务器获取用户信息成功:', res)
if (res.success && res.data) {
const serverUserInfo = res.data
// 检查手机号是否是临时手机号
if (serverUserInfo.phoneNumber === '13800138000') {
console.warn('服务器返回的仍是临时手机号,用户可能需要重新授权手机号')
// 可以在这里显示提示,让用户重新授权手机号
this.setData({
needPhoneAuth: true
})
}
// 更新本地用户信息
const app = getApp()
const updatedUserInfo = {
...app.globalData.userInfo,
...serverUserInfo
}
app.globalData.userInfo = updatedUserInfo
wx.setStorageSync('userInfo', updatedUserInfo)
this.setData({ userInfo: updatedUserInfo })
console.log('用户信息已更新,昵称:', updatedUserInfo.nickName, '手机号:', updatedUserInfo.phoneNumber)
}
}).catch(err => {
console.error('从服务器获取用户信息失败:', err)
// 如果getUserInfo失败,尝试使用validateUserLogin作为备选
API.validateUserLogin().then(res => {
console.log('使用validateUserLogin获取用户信息成功:', res)
if (res.success && res.data) {
const serverUserInfo = res.data
// 检查手机号是否是临时手机号
if (serverUserInfo.phoneNumber === '13800138000') {
console.warn('服务器返回的仍是临时手机号,用户可能需要重新授权手机号')
this.setData({
needPhoneAuth: true
})
}
// 更新本地用户信息
const app = getApp()
const updatedUserInfo = {
...app.globalData.userInfo,
...serverUserInfo
}
app.globalData.userInfo = updatedUserInfo
wx.setStorageSync('userInfo', updatedUserInfo)
this.setData({ userInfo: updatedUserInfo })
console.log('用户信息已更新(备选方案):', updatedUserInfo)
}
}).catch(validateErr => {
console.error('从服务器获取用户信息失败(包括备选方案):', validateErr)
// 如果服务器请求失败,继续使用本地缓存的信息
})
})
},
// 格式化用户类型显示
formatUserType(type) {
switch (type) {
case 'buyer': return '买家'
case 'seller': return '卖家'
case 'both': return '买家+卖家'
default: return '未设置'
}
},
// 设置为买家
setAsBuyer() {
this.switchUserType('buyer', '买家')
},
// 设置为卖家
setAsSeller() {
this.switchUserType('seller', '卖家')
},
// 切换用户类型的通用方法
switchUserType(newType, typeName) {
const userId = wx.getStorageSync('userId')
const openid = wx.getStorageSync('openid')
const userInfo = wx.getStorageSync('userInfo')
if (!userId || !openid) {
wx.navigateTo({ url: '/pages/index/index' })
return
}
// 更新本地存储中的用户类型
let users = wx.getStorageSync('users') || {}
if (!users[userId]) {
users[userId] = {}
}
users[userId].type = newType
wx.setStorageSync('users', users)
// 更新全局数据
const app = getApp()
app.globalData.userType = newType
// 上传更新后的用户信息到服务器
this.uploadUserTypeToServer(openid, userId, userInfo, newType)
// 更新页面显示
this.setData({
userType: this.formatUserType(newType)
})
wx.showToast({
title: `已切换为${typeName}`,
icon: 'success',
duration: 2000
})
},
// 上传用户类型到服务器
uploadUserTypeToServer(openid, userId, userInfo, type) {
// 引入API服务
const API = require('../../utils/api.js')
// 构造上传数据
const uploadData = {
userId: userId,
openid: openid,
...userInfo,
type: type,
timestamp: Date.now()
}
// 调用API上传用户信息
API.uploadUserInfo(uploadData).then(res => {
console.log('用户类型更新成功:', res)
}).catch(err => {
console.error('用户类型更新失败:', err)
wx.showToast({
title: '身份更新失败,请重试',
icon: 'none',
duration: 2000
})
})
},
// 处理手机号授权结果
onPhoneNumberResult(e) {
console.log('手机号授权结果:', e)
// 首先检查用户是否拒绝授权
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
console.log('用户拒绝授权手机号')
wx.showToast({
title: '您已拒绝授权,操作已取消',
icon: 'none'
})
// 直接返回,取消所有后续操作
return
}
// 用户同意授权,继续执行后续操作
// 检查是否有openid,如果没有则先登录
const openid = wx.getStorageSync('openid')
if (!openid) {
console.log('未登录,执行登录流程')
// 显示登录loading提示
wx.showLoading({ title: '登录中...' })
// 调用微信登录接口
wx.login({
success: loginRes => {
if (loginRes.code) {
// 引入API服务
const API = require('../../utils/api.js')
// 获取openid
API.getOpenid(loginRes.code)
.then(openidRes => {
wx.hideLoading()
console.log('获取openid响应:', openidRes)
// 增强版响应处理逻辑,支持多种返回格式
let openid = null;
let userId = null;
let sessionKey = null;
// 优先从data字段获取数据
if (openidRes && openidRes.data && typeof openidRes.data === 'object') {
openid = openidRes.data.openid || openidRes.data.OpenID || null;
userId = openidRes.data.userId || openidRes.data.userid || null;
sessionKey = openidRes.data.session_key || openidRes.data.sessionKey || null;
}
// 如果data为空或不存在,尝试从响应对象直接获取
if (!openid && openidRes && typeof openidRes === 'object') {
console.warn('服务器返回格式可能不符合预期,data字段为空或不存在,但尝试从根对象提取信息:', openidRes);
openid = openidRes.openid || openidRes.OpenID || null;
userId = openidRes.userId || openidRes.userid || null;
sessionKey = openidRes.session_key || openidRes.sessionKey || null;
}
// 检查服务器状态信息
const isSuccess = openidRes && (openidRes.success === true || openidRes.code === 200);
if (openid) {
// 存储openid和session_key
wx.setStorageSync('openid', openid)
if (sessionKey) {
wx.setStorageSync('sessionKey', sessionKey)
}
// 如果有userId,也存储起来
if (userId) {
wx.setStorageSync('userId', userId)
}
console.log('获取openid成功并存储:', openid)
// 登录成功,显示提示并重新加载页面
wx.showToast({
title: '登录成功',
icon: 'none'
})
// 在登录成功后重新加载页面
wx.reLaunch({
url: '/pages/profile/index'
})
} else {
console.error('获取openid失败,响应数据:', openidRes)
wx.showToast({
title: '登录失败,请重试',
icon: 'none'
})
}
})
.catch(err => {
wx.hideLoading()
console.error('获取openid失败:', err)
wx.showToast({
title: '登录失败,请重试',
icon: 'none'
})
})
} else {
wx.hideLoading()
console.error('微信登录失败:', loginRes)
wx.showToast({
title: '登录失败,请重试',
icon: 'none'
})
}
},
fail: err => {
wx.hideLoading()
console.error('wx.login失败:', err)
wx.showToast({
title: '获取登录状态失败,操作已取消',
icon: 'none'
})
}
})
return
}
// 已登录且用户同意授权,获取加密数据
const phoneData = e.detail
wx.showLoading({ title: '获取手机号中...' })
// 引入API服务
const API = require('../../utils/api.js')
// 上传到服务器解密
API.uploadPhoneNumberData(phoneData)
.then(res => {
wx.hideLoading()
if (res.success) {
console.log('获取手机号结果:', res)
// 检查是否有手机号冲突
const hasPhoneConflict = res.phoneNumberConflict || false
const isNewPhone = res.isNewPhone || true
const phoneNumber = res.phoneNumber || null
// 如果有手机号冲突且没有返回手机号,使用临时手机号
const finalPhoneNumber = hasPhoneConflict && !phoneNumber ? '13800138000' : phoneNumber
if (finalPhoneNumber) {
// 保存手机号到用户信息
const app = getApp()
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {}
userInfo.phoneNumber = finalPhoneNumber
// 更新本地和全局用户信息
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
// 获取userId
const userId = wx.getStorageSync('userId')
const users = wx.getStorageSync('users') || {}
const currentUserType = users[userId] && users[userId].type ? users[userId].type : ''
// 同时更新服务器用户信息
this.uploadUserInfoToServer(userInfo, userId, currentUserType)
// 更新页面状态
this.setData({
needPhoneAuth: finalPhoneNumber === '13800138000'
})
// 重新加载用户信息以更新UI
this.loadUserInfo()
}
// 根据服务器返回的结果显示不同的提示
if (hasPhoneConflict) {
wx.showToast({
title: '获取成功,但手机号已被其他账号绑定',
icon: 'none',
duration: 3000
})
} else {
wx.showToast({
title: '手机号绑定成功',
icon: 'success'
})
}
} else {
console.error('获取手机号失败:', res)
wx.showToast({
title: '获取手机号失败',
icon: 'none'
})
}
})
.catch(err => {
wx.hideLoading()
console.error('获取手机号失败:', err)
wx.showToast({
title: '获取手机号失败',
icon: 'none'
})
})
},
// 上传用户信息到服务器
uploadUserInfoToServer(userInfo, userId, type) {
// 返回Promise以便调用者可以进行错误处理
return new Promise((resolve, reject) => {
try {
// 引入API服务
const API = require('../../utils/api.js')
// 获取openid
const openid = wx.getStorageSync('openid')
// 验证必要参数
if (!userId || !openid) {
const error = new Error('缺少必要的用户信息');
console.error('用户信息上传失败:', error);
reject(error);
return;
}
// 构造上传数据(包含所有必要字段,包括phoneNumber)
const uploadData = {
userId: userId,
openid: openid,
nickName: userInfo.nickName,
phoneNumber: userInfo.phoneNumber, // 添加phoneNumber字段,满足服务器要求
type: type,
timestamp: Date.now()
}
// 调用API上传用户信息
API.uploadUserInfo(uploadData).then(res => {
console.log('用户信息上传成功:', res)
resolve(res);
}).catch(err => {
console.error('用户信息上传失败:', err)
reject(err);
})
} catch (error) {
console.error('上传用户信息时发生异常:', error);
reject(error);
}
});
},
// 修改用户名称
onEditNickName() {
const currentName = this.data.userInfo.nickName || '未登录';
wx.showModal({
title: '修改用户名称',
editable: true,
placeholderText: '请输入新的名称最多10个字符',
confirmText: '确认',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 移除首尾空格并过滤多余空格
let newName = res.content.trim();
newName = newName.replace(/\s+/g, ' ');
// 验证昵称不能为空
if (!newName) {
wx.showToast({
title: '名称不能为空',
icon: 'none',
duration: 2000
});
return;
}
// 验证昵称长度
if (newName.length > 10) {
wx.showToast({
title: '名称长度不能超过10个字符',
icon: 'none',
duration: 2000
});
return;
}
// 检查名称是否变化
if (newName === currentName) {
wx.showToast({
title: '名称未变化',
icon: 'none',
duration: 2000
});
return;
}
// 显示加载提示
wx.showLoading({
title: '正在更新...',
mask: true
});
// 更新用户信息
this.updateNickName(newName).finally(() => {
// 无论成功失败,都隐藏加载提示
wx.hideLoading();
});
}
}
});
},
// 更新用户名称
updateNickName(newName) {
return new Promise((resolve, reject) => {
try {
// 更新本地和全局用户信息
const app = getApp();
const updatedUserInfo = {
...this.data.userInfo,
nickName: newName
};
// 保存到本地存储和全局状态
app.globalData.userInfo = updatedUserInfo;
wx.setStorageSync('userInfo', updatedUserInfo);
// 更新页面显示
this.setData({
userInfo: updatedUserInfo
});
// 更新服务器信息
const userId = wx.getStorageSync('userId');
const currentUserType = this.data.userType;
// 如果有用户ID,则上传到服务器
if (userId) {
// 使用Promise链处理上传
this.uploadUserInfoToServer(updatedUserInfo, userId, currentUserType)
.then(() => {
wx.showToast({
title: '名称修改成功',
icon: 'success',
duration: 2000
});
resolve();
})
.catch((err) => {
console.error('服务器同步失败,但本地已更新:', err);
// 即使服务器同步失败,本地也已成功更新
wx.showToast({
title: '本地更新成功,服务器同步稍后进行',
icon: 'none',
duration: 3000
});
resolve(); // 即使服务器失败,也视为成功,因为本地已经更新
});
} else {
// 没有用户ID,只更新本地
console.warn('没有用户ID,仅更新本地用户名称');
wx.showToast({
title: '名称修改成功',
icon: 'success',
duration: 2000
});
resolve();
}
} catch (error) {
console.error('更新用户名称失败:', error);
wx.showToast({
title: '更新失败,请稍后重试',
icon: 'none',
duration: 2000
});
reject(error);
}
});
},
})

3
pages/profile/index.json

@ -0,0 +1,3 @@
{
"usingComponents": {}
}

63
pages/profile/index.wxml

@ -0,0 +1,63 @@
<view class="container" style="align-items: flex-start; padding: 20rpx; width: 100%; max-width: 100vw; overflow-x: hidden; position: relative; box-sizing: border-box;">
<view class="card" style="display: flex; align-items: center; justify-content: space-between;">
<view style="display: flex; align-items: center;">
<image
src="{{userInfo.avatarUrl || '/images/default-avatar.png'}}"
style="width: 100rpx; height: 100rpx; border-radius: 50%; margin-right: 20rpx;"
></image>
<view>
<view style="font-size: 32rpx; font-weight: bold;">{{userInfo.nickName || '未登录'}}</view>
<view style="font-size: 26rpx; color: #666;">当前身份: {{userType || '未设置'}}</view>
<view style="font-size: 26rpx; color: {{userInfo.phoneNumber === '13800138000' ? '#ff4d4f' : '#666'}};">
手机号: {{userInfo.phoneNumber === '13800138000' ? '临时手机号,请重新授权' : (userInfo.phoneNumber || '未绑定')}}
</view>
</view>
</view>
<button
class="edit-btn"
bindtap="onEditNickName"
style="background-color: #1677ff; color: white; padding: 10rpx 20rpx; border-radius: 20rpx; font-size: 26rpx;"
>
修改名称
</button>
</view>
<!-- 手机号授权按钮 -->
<view class="card" wx:if="{{!userInfo.phoneNumber || userInfo.phoneNumber === '13800138000'}}">
<button
open-type="getPhoneNumber"
bindgetphonenumber="onPhoneNumberResult"
type="primary"
style="margin: 20rpx 0;"
>
{{userInfo.phoneNumber === '13800138000' ? '重新授权手机号' : '授权手机号'}}
</button>
</view>
<view class="card">
<view class="title">我的标签</view>
<view style="flex-wrap: wrap; display: flex;">
<view wx:for="{{userTags}}" wx:key="index" style="background-color: #f0f2f5; padding: 10rpx 20rpx; border-radius: 20rpx; margin: 10rpx; font-size: 26rpx;">
{{item}}
</view>
</view>
</view>
<view class="card" wx:if="{{false}}">
<view class="title">身份管理</view>
<button
class="btn"
style="background-color: {{userType === 'buyer' || userType === 'both' ? '#888' : '#07c160'}}; color: white;"
bindtap="setAsBuyer"
>
{{userType === 'buyer' || userType === 'both' ? '已设为买家' : '设为买家'}}
</button>
<button
class="btn"
style="background-color: {{userType === 'seller' || userType === 'both' ? '#888' : '#1677ff'}}; color: white;"
bindtap="setAsSeller"
>
{{userType === 'seller' || userType === 'both' ? '已设为卖家' : '设为卖家'}}
</button>
</view>
</view>

1
pages/profile/index.wxss

@ -0,0 +1 @@
/* pages/profile/index.wxss */

588
pages/publish/index.js

@ -0,0 +1,588 @@
// pages/publish/index.js
// 引入API工具
const API = require('../../utils/api.js');
// 【终极修复】创建全局上传管理器,完全独立于页面生命周期
if (!global.ImageUploadManager) {
global.ImageUploadManager = {
// 存储所有活动的上传任务
activeTasks: {},
// 深度克隆工具函数
deepClone: function(obj) {
return JSON.parse(JSON.stringify(obj));
},
// 核心上传方法
upload: function(formData, images, successCallback, failCallback) {
console.log('【全局上传管理器】开始上传,图片数量:', images.length);
// 创建唯一的上传任务ID
const taskId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 创建深度克隆,完全隔离数据
const clonedFormData = this.deepClone(formData);
const clonedImages = this.deepClone(images);
// 将任务保存到全局状态中,防止页面重新编译时丢失
this.activeTasks[taskId] = {
id: taskId,
formData: clonedFormData,
images: clonedImages,
status: 'started',
startTime: Date.now(),
uploadedCount: 0,
totalCount: clonedImages.length
};
console.log(`【全局上传管理器】创建任务 ${taskId},已保存到全局状态`);
// 使用setTimeout完全隔离执行上下文,避免与页面生命周期耦合
const self = this;
setTimeout(() => {
try {
console.log('【全局上传管理器】准备调用API.publishProduct');
console.log('准备的商品数据:', clonedFormData);
console.log('准备的图片数量:', clonedImages.length);
// 关键修改:使用API.publishProduct方法,这是正确的调用链
// 包含所有必要字段
const productData = {
...clonedFormData,
images: clonedImages, // 直接传递图片数组
imageUrls: clonedImages, // 同时设置imageUrls字段
// 生成会话ID,确保所有图片关联同一商品
sessionId: taskId,
uploadSessionId: taskId
};
console.log('最终传递给publishProduct的数据:', Object.keys(productData));
API.publishProduct(productData)
.then(res => {
console.log(`【全局上传管理器】任务 ${taskId} 上传完成,响应:`, res);
// 更新任务状态
if (self.activeTasks[taskId]) {
self.activeTasks[taskId].status = 'completed';
self.activeTasks[taskId].endTime = Date.now();
self.activeTasks[taskId].result = res;
}
// 使用setTimeout隔离成功回调的执行
setTimeout(() => {
if (successCallback) {
successCallback(res);
}
}, 0);
})
.catch(err => {
console.error(`【全局上传管理器】任务 ${taskId} 上传失败:`, err);
// 更新任务状态
if (self.activeTasks[taskId]) {
self.activeTasks[taskId].status = 'failed';
self.activeTasks[taskId].error = err;
self.activeTasks[taskId].endTime = Date.now();
}
// 使用setTimeout隔离失败回调的执行
setTimeout(() => {
if (failCallback) {
failCallback(err);
}
}, 0);
})
.finally(() => {
// 延迟清理任务,确保所有操作完成
setTimeout(() => {
if (self.activeTasks[taskId]) {
delete self.activeTasks[taskId];
console.log(`【全局上传管理器】任务 ${taskId} 已清理`);
}
}, 10000);
});
} catch (e) {
console.error(`【全局上传管理器】任务 ${taskId} 发生异常:`, e);
setTimeout(() => {
if (failCallback) {
failCallback(e);
}
}, 0);
}
}, 0);
return taskId;
},
// 获取任务状态的方法
getTaskStatus: function(taskId) {
return this.activeTasks[taskId] || null;
},
// 获取所有活动任务
getActiveTasks: function() {
return Object.values(this.activeTasks);
}
};
}
Page({
/**
* 页面的初始数据
*/
data: {
variety: '', // 品种
price: '',
quantity: '',
grossWeight: '',
yolk: '', // 蛋黄
specification: '',
images: [] // 新增图片数组
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 检查用户是否已登录
this.checkLoginStatus();
},
/**
* 检查用户登录状态
*/
checkLoginStatus() {
const openid = wx.getStorageSync('openid');
if (!openid) {
wx.showModal({
title: '提示',
content: '请先登录后再发布商品',
showCancel: false,
success: (res) => {
if (res.confirm) {
wx.navigateTo({ url: '/pages/index/index' });
}
}
});
}
},
/**
* 品种输入处理
*/
onVarietyInput(e) {
this.setData({
variety: e.detail.value
});
},
/**
* 蛋黄输入处理
*/
onYolkInput(e) {
this.setData({
yolk: e.detail.value
});
},
/**
* 价格输入处理
*/
onPriceInput(e) {
// 保存原始字符串值,不进行数字转换
this.setData({
price: e.detail.value
});
},
/**
* 数量输入处理
*/
onQuantityInput(e) {
const value = parseFloat(e.detail.value);
this.setData({
quantity: isNaN(value) ? '' : value
});
},
/**
* 毛重输入处理
*/
onGrossWeightInput(e) {
// 直接保存原始字符串值,不进行数字转换
this.setData({
grossWeight: e.detail.value
});
},
/**
* 规格输入处理
*/
onSpecificationInput(e) {
this.setData({
specification: e.detail.value
});
},
/**
* 表单验证
*/
validateForm() {
const { variety, price, quantity } = this.data;
console.log('表单验证数据 - variety:', variety, 'price:', price, 'quantity:', quantity);
console.log('数据类型 - variety:', typeof variety, 'price:', typeof price, 'quantity:', typeof quantity);
if (!variety || !variety.trim()) {
wx.showToast({ title: '请输入品种', icon: 'none' });
return false;
}
if (!price || price.trim() === '') {
wx.showToast({ title: '请输入有效价格', icon: 'none' });
return false;
}
if (quantity === '' || quantity === undefined || quantity === null || quantity <= 0) {
wx.showToast({ title: '请输入有效数量', icon: 'none' });
return false;
}
console.log('表单验证通过');
return true;
},
/**
* 发布商品按钮点击事件
*/
onPublishTap() {
console.log('发布按钮点击');
// 检查用户登录状态
const openid = wx.getStorageSync('openid');
const userInfo = wx.getStorageSync('userInfo');
const userId = wx.getStorageSync('userId');
console.log('检查用户授权状态 - openid:', !!openid, 'userInfo:', !!userInfo, 'userId:', !!userId);
if (!openid || !userId || !userInfo) {
console.log('用户未登录或未授权,引导重新登录');
wx.showModal({
title: '登录过期',
content: '请先授权登录后再发布商品',
showCancel: false,
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
wx.navigateTo({ url: '/pages/index/index' });
}
}
});
return;
}
if (!this.validateForm()) {
console.log('表单验证失败');
return;
}
const { variety, price, quantity, grossWeight, yolk, specification } = this.data;
const images = this.data.images;
// 构建商品数据,确保价格和数量为字符串类型
const productData = {
productName: variety.trim(), // 使用品种作为商品名称
price: price.toString(),
quantity: quantity.toString(),
grossWeight: grossWeight !== '' && grossWeight !== null && grossWeight !== undefined ? grossWeight : "",
yolk: yolk || '',
specification: specification || '',
images: images,
imageUrls: images,
allImageUrls: images,
hasMultipleImages: images.length > 1,
totalImages: images.length
};
console.log('【关键日志】商品数据:', productData);
console.log('【关键日志】图片数量:', images.length);
// 【终极修复】在上传开始前立即清空表单
// 先深度克隆所有数据
console.log('【上传前检查】准备克隆数据');
const formDataCopy = JSON.parse(JSON.stringify(productData));
const imagesCopy = JSON.parse(JSON.stringify(images));
console.log('【上传前检查】克隆后图片数量:', imagesCopy.length);
console.log('【上传前检查】克隆后图片数据:', imagesCopy);
// 立即清空表单,避免任何状态变化触发重新编译
console.log('【上传前检查】清空表单');
this.setData({
variety: '',
price: '',
quantity: '',
grossWeight: '',
yolk: '',
specification: '',
images: []
});
// 显示加载提示
wx.showLoading({ title: '正在上传图片...' });
// 【终极修复】使用全局上传管理器处理上传,完全脱离页面生命周期
// 将所有数据存储到全局对象中,防止被回收
console.log('【上传前检查】存储数据到全局对象');
global.tempUploadData = {
formData: formDataCopy,
images: imagesCopy,
userId: userId,
timestamp: Date.now()
};
// 预先生成会话ID,确保所有图片关联同一个商品
const uploadSessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
formDataCopy.sessionId = uploadSessionId;
formDataCopy.uploadSessionId = uploadSessionId;
console.log(`【关键修复】预先生成会话ID:`, uploadSessionId);
console.log(`【上传前检查】准备调用全局上传管理器,图片数量:`, imagesCopy.length);
console.log(`【上传前检查】传递的formData结构:`, Object.keys(formDataCopy));
// 【核心修复】直接使用wx.uploadFile API,确保与服务器端测试脚本格式一致
console.log(`【核心修复】使用wx.uploadFile API直接上传`);
// 预先生成会话ID
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
formDataCopy.sessionId = sessionId;
formDataCopy.uploadSessionId = sessionId;
console.log(`【核心修复】使用会话ID:`, sessionId);
console.log(`【核心修复】上传图片数量:`, imagesCopy.length);
// 使用Promise处理上传
const uploadPromise = new Promise((resolve, reject) => {
// 构建formData,与服务器测试脚本一致
const formData = {
productData: JSON.stringify(formDataCopy),
sessionId: sessionId,
uploadSessionId: sessionId,
totalImages: imagesCopy.length.toString(),
isSingleUpload: 'false' // 关键参数:标记为多图片上传
};
console.log(`【核心修复】准备上传,formData结构:`, Object.keys(formData));
console.log(`【核心修复】上传URL:`, API.BASE_URL + '/api/products/upload');
// 直接使用wx.uploadFile上传第一张图片
wx.uploadFile({
url: API.BASE_URL + '/api/products/upload',
filePath: imagesCopy[0], // 先上传第一张
name: 'images',
formData: formData,
timeout: 180000,
success: (res) => {
console.log('【核心修复】上传成功,状态码:', res.statusCode);
console.log('【核心修复】原始响应:', res.data);
try {
const data = JSON.parse(res.data);
resolve(data);
} catch (e) {
resolve({data: res.data});
}
},
fail: (err) => {
console.error('【核心修复】上传失败:', err);
reject(err);
}
});
});
uploadPromise.then((res) => {
// 上传成功回调
console.log('【核心修复】上传成功,响应:', res);
// 使用setTimeout完全隔离回调执行上下文
setTimeout(() => {
wx.hideLoading();
// 从全局临时存储获取数据
const tempData = global.tempUploadData || {};
const localFormData = tempData.formData;
const userId = tempData.userId;
// 【关键修复】从多个来源提取图片URL,确保不丢失
let allUploadedImageUrls = [];
// 尝试从多个位置提取图片URLs
if (res.imageUrls && Array.isArray(res.imageUrls) && res.imageUrls.length > 0) {
allUploadedImageUrls = [...res.imageUrls];
console.log('【全局上传】从res.imageUrls提取到图片:', allUploadedImageUrls.length);
}
if (res.product && res.product.imageUrls && Array.isArray(res.product.imageUrls) && res.product.imageUrls.length > 0) {
allUploadedImageUrls = [...res.product.imageUrls];
console.log('【全局上传】从res.product.imageUrls提取到图片:', allUploadedImageUrls.length);
}
if (res.data && res.data.imageUrls && Array.isArray(res.data.imageUrls) && res.data.imageUrls.length > 0) {
allUploadedImageUrls = [...res.data.imageUrls];
console.log('【全局上传】从res.data.imageUrls提取到图片:', allUploadedImageUrls.length);
}
// 去重处理,确保URL不重复
allUploadedImageUrls = [...new Set(allUploadedImageUrls)];
console.log('【全局上传】最终去重后的图片URL列表:', allUploadedImageUrls);
console.log('【全局上传】最终图片数量:', allUploadedImageUrls.length);
// 获取卖家信息
const users = wx.getStorageSync('users') || {};
const sellerName = users[userId] && users[userId].info && users[userId].info.nickName ? users[userId].info.nickName : '未知卖家';
// 保存到本地存储
setTimeout(() => {
// 获取当前已有的货源列表
const supplies = wx.getStorageSync('supplies') || [];
const newId = supplies.length > 0 ? Math.max(...supplies.map(s => s.id)) + 1 : 1;
const serverProductId = res.product && res.product.productId ? res.product.productId : '';
// 创建新的货源记录
const newSupply = {
id: newId,
productId: serverProductId,
serverProductId: serverProductId,
name: localFormData.productName,
productName: localFormData.productName,
price: localFormData.price,
minOrder: localFormData.quantity,
yolk: localFormData.yolk,
spec: localFormData.specification,
grossWeight: localFormData.grossWeight !== null ? localFormData.grossWeight : '',
seller: sellerName,
status: res.product && res.product.status ? res.product.status : 'pending_review',
imageUrls: allUploadedImageUrls,
reservedCount: 0,
isReserved: false
};
// 保存到supplies和goods本地存储
supplies.push(newSupply);
wx.setStorageSync('supplies', supplies);
const goods = wx.getStorageSync('goods') || [];
const newGoodForBuyer = {
id: String(newId),
productId: String(serverProductId),
name: localFormData.productName,
productName: localFormData.productName,
price: localFormData.price,
minOrder: localFormData.quantity,
yolk: localFormData.yolk,
spec: localFormData.specification,
grossWeight: localFormData.grossWeight !== null ? localFormData.grossWeight : '',
displayGrossWeight: localFormData.grossWeight !== null ? localFormData.grossWeight : '',
seller: sellerName,
status: res.product && res.product.status ? res.product.status : 'pending_review',
imageUrls: allUploadedImageUrls,
reservedCount: 0,
isReserved: false
};
goods.push(newGoodForBuyer);
wx.setStorageSync('goods', goods);
// 显示成功提示
setTimeout(() => {
wx.showModal({
title: '发布成功',
content: `所有${allUploadedImageUrls.length}张图片已成功上传!\n请手动返回查看您的商品。\n\n重要:请勿关闭小程序,等待3-5秒确保所有数据处理完成。`,
showCancel: false,
confirmText: '我知道了',
success: function() {
// 延迟清理全局临时数据,确保所有操作完成
setTimeout(() => {
if (global.tempUploadData) {
delete global.tempUploadData;
}
}, 5000);
}
});
}, 500);
}, 500);
}, 100);
})
.catch((err) => {
// 上传失败回调
console.error('【核心修复】上传失败:', err);
// 使用setTimeout隔离错误处理
setTimeout(() => {
wx.hideLoading();
if (err.needRelogin) {
wx.showModal({
title: '登录状态失效',
content: '请重新授权登录',
showCancel: false,
success: (res) => {
if (res.confirm) {
wx.removeStorageSync('openid');
wx.removeStorageSync('userId');
wx.navigateTo({ url: '/pages/login/index' });
}
}
});
} else {
wx.showToast({ title: err.message || '发布失败,请重试', icon: 'none' });
}
// 清理全局临时数据
if (global.tempUploadData) {
delete global.tempUploadData;
}
}, 100);
});
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 页面显示时可以刷新数据
},
/**
* 选择图片 - 修复版本
*/
chooseImage: function () {
const that = this;
wx.chooseMedia({
count: 5 - that.data.images.length,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: function (res) {
console.log('选择图片成功,返回数据:', res);
const tempFiles = res.tempFiles.map(file => file.tempFilePath);
that.setData({
images: [...that.data.images, ...tempFiles]
});
console.log('更新后的图片数组:', that.data.images);
},
fail: function (err) {
console.error('选择图片失败:', err);
}
});
},
/**
* 删除图片
*/
deleteImage: function (e) {
const index = e.currentTarget.dataset.index;
const images = this.data.images;
images.splice(index, 1);
this.setData({
images: images
});
}
});

3
pages/publish/index.json

@ -0,0 +1,3 @@
{
"usingComponents": {}
}

54
pages/publish/index.wxml

@ -0,0 +1,54 @@
<!--pages/publish/index.wxml-->
<view class="publish-container">
<view class="publish-header">
<text class="header-title">发布新货源</text>
</view>
<view class="form-container">
<view class="form-item">
<text class="label">品种 *</text>
<input class="input" type="text" placeholder="请输入品种" bindinput="onVarietyInput" value="{{variety}}" />
</view>
<view class="form-item">
<text class="label">价格 (元/斤) *</text>
<input class="input" type="text" placeholder="请输入商品价格(支持文字描述)" bindinput="onPriceInput" value="{{price}}" />
</view>
<view class="form-item">
<text class="label">数量 (斤) *</text>
<input class="input" type="digit" placeholder="请输入商品数量" bindinput="onQuantityInput" value="{{quantity}}" />
</view>
<view class="form-item">
<text class="label">毛重 (斤)</text>
<input class="input" type="text" placeholder="请输入商品毛重(可输入文字,如:十斤)" bindinput="onGrossWeightInput" value="{{grossWeight}}" />
</view>
<view class="form-item">
<text class="label">蛋黄</text>
<input class="input" type="text" placeholder="请输入蛋黄信息" bindinput="onYolkInput" value="{{yolk}}" />
</view>
<view class="form-item">
<text class="label">规格</text>
<input class="input" type="text" placeholder="请输入商品规格" bindinput="onSpecificationInput" value="{{specification}}" />
</view>
<!-- 新增图片上传区域 -->
<view class="image-upload-container">
<text class="label">商品图片(最多5张)</text>
<view class="image-list">
<view class="image-item" wx:for="{{images}}" wx:key="index">
<image src="{{item}}" mode="aspectFill"></image>
<view class="image-delete" bindtap="deleteImage" data-index="{{index}}">×</view>
</view>
<view class="image-upload" wx:if="{{images.length < 5}}" bindtap="chooseImage">
<text>+</text>
</view>
</view>
</view>
<button class="publish-btn" type="primary" bindtap="onPublishTap">发布商品</button>
</view>
</view>

117
pages/publish/index.wxss

@ -0,0 +1,117 @@
/* pages/publish/index.wxss */
.publish-container {
padding: 20rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
.publish-header {
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.header-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.form-container {
background-color: #fff;
padding: 30rpx;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 10rpx;
}
.input {
width: 100%;
height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
/* 图片上传样式 */
.image-upload-container {
margin-bottom: 30rpx;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.image-item {
width: 160rpx;
height: 160rpx;
position: relative;
border: 1rpx solid #ddd;
border-radius: 8rpx;
overflow: hidden;
}
.image-item image {
width: 100%;
height: 100%;
}
.image-delete {
position: absolute;
top: 0;
right: 0;
width: 40rpx;
height: 40rpx;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
text-align: center;
line-height: 40rpx;
font-size: 32rpx;
border-radius: 0 8rpx 0 20rpx;
}
.image-upload {
width: 160rpx;
height: 160rpx;
border: 2rpx dashed #ddd;
border-radius: 8rpx;
display: flex;
justify-content: center;
align-items: center;
background-color: #f8f8f8;
}
.image-upload text {
font-size: 64rpx;
color: #999;
}
.publish-btn {
margin-top: 40rpx;
background-color: #07c160;
color: #fff;
font-size: 32rpx;
height: 90rpx;
line-height: 90rpx;
border-radius: 45rpx;
}
.publish-btn:active {
background-color: #06b356;
}

3333
pages/seller/index.js

File diff suppressed because it is too large

5
pages/seller/index.json

@ -0,0 +1,5 @@
{
"usingComponents": {},
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}

833
pages/seller/index.wxml

@ -0,0 +1,833 @@
<view class="container {{pageScrollLock ? 'page-scroll-lock' : ''}}"
style="align-items: flex-start; padding: 20rpx; width: 100%; max-width: 100vw; overflow-x: hidden; position: relative; box-sizing: border-box;"
catchtouchmove="{{touchMoveBlocked ? 'preventTouchMove' : ''}}">
<view style="display: flex; justify-content: space-between; align-items: center; width: 90%; margin-bottom: 20rpx;">
<view class="title">我的鸡蛋货源</view>
<button
bindtap="contactCustomerService"
class="customer-service-btn"
>
联系客服
</button>
</view>
<!-- 搜索框 -->
<view style="width: 100%; display: flex; justify-content: center; margin-bottom: 20rpx;">
<view style="width: 90%; display: flex; border: 1rpx solid #ddd; border-radius: 40rpx; overflow: hidden;">
<input
style="flex: 1; padding: 20rpx 30rpx;"
placeholder="搜索货源名称或品种"
bindinput="onSearchInput"
value="{{searchKeyword}}"
/>
<button
style="background-color: #52c41a; color: white; font-size: 26rpx; height: 80rpx; line-height: 80rpx; padding: 0 30rpx;"
bindtap="searchSupplies"
>
搜索
</button>
</view>
</view>
<button
class="glass-btn primary-glass-btn"
bindtap="showAddSupply"
style="width: 90%;"
>
创建新货源
</button>
<!-- 已上架货源 -->
<view style="margin-top: 30rpx; width: 100%;">
<view style="font-size: 28rpx; font-weight: bold; color: #52c41a; margin-bottom: 15rpx; display: flex; justify-content: space-between; align-items: center;">
<text>已上架货源 ({{publishedSupplies.length}})</text>
<view bindtap="togglePublishedExpand" style="width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center;">
<text wx:if="{{isPublishedExpanded}}" style="color: #52c41a; font-size: 28rpx;">▼</text>
<text wx:else style="color: #52c41a; font-size: 28rpx;">▲</text>
</view>
</view>
<block wx:if="{{isPublishedExpanded}}">
<block wx:if="{{publishedSupplies.length > 0}}">
<view wx:for="{{publishedSupplies}}" wx:key="id" class="card" style="width: 100%;">
<!-- 图片和信息1:1比例并排显示 -->
<view style="display: flex; width: 100%; border-radius: 8rpx; overflow: hidden; background-color: #f5f5f5;">
<!-- 左侧图片区域 50%宽度 -->
<view style="width: 50%; position: relative;">
<!-- 第一张图片 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 0}}" style="width: 100%; height: 100%;">
<image src="{{item.imageUrls[0]}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="0" binderror="imageError" bindload="imageLoad"
loading="lazy"
fallback-src="../../images/logo.svg">
</image>
</view>
<view wx:else style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999;">
<text>暂无图片</text>
</view>
<!-- 剩余图片可滑动区域 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 1}}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
<swiper
class="image-swiper"
style="width: 100%; height: 100%;"
current="{{item.currentImageIndex || 0}}"
bindchange="swiperChange"
data-id="{{item.id}}">
<block wx:for="{{item.imageUrls}}" wx:for-item="img" wx:for-index="idx" wx:key="idx">
<swiper-item>
<image src="{{img}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="{{idx}}"></image>
</swiper-item>
</block>
</swiper>
<!-- 显示页码指示器 -->
<view style="position: absolute; bottom: 10rpx; right: 10rpx; background-color: rgba(0,0,0,0.5); color: white; padding: 5rpx 10rpx; border-radius: 15rpx; font-size: 20rpx;">
{{(item.currentImageIndex || 0) + 1}}/{{item.imageUrls.length}}
</view>
</view>
</view>
<!-- 右侧信息区域 50%宽度 -->
<view style="width: 50%; padding: 15rpx; display: flex; flex-direction: column; justify-content: space-between; background-color: white; border-left: 1rpx solid #f0f0f0;">
<view>
<view style="font-size: 28rpx; font-weight: bold; word-break: break-word;">{{item.name}}
<view style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #52c41a; padding: 2rpx 8rpx; border-radius: 10rpx;">已上架</view>
</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">蛋黄: {{item.yolk || '无'}}</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">规格: {{item.spec || '无'}}</view>
<view style="color: #f5222d; font-size: 24rpx; margin-top: 8rpx;">件数: {{item.minOrder}}件</view>
<view style="color: #1677ff; font-size: 24rpx; margin-top: 8rpx;">斤重: {{item.grossWeight || ''}}斤</view>
<view style="color: #722ed1; font-size: 24rpx; margin-top: 8rpx;">地区: {{item.region || '未设置'}}</view>
<view style="font-size: 22rpx; color: #999; margin-top: 8rpx;">创建时间: {{item.formattedCreatedAt}}</view>
</view>
<!-- 按钮区域 -->
<view style="display: flex; justify-content: center; align-items: center; margin-top: 10rpx;">
<button
style="background-color: #f5222d; color: white; font-size: 22rpx; padding: 0 20rpx; line-height: 60rpx;"
bindtap="unpublishSupply"
data-id="{{item.id}}"
>
下架
</button>
</view>
</view>
</view>
</view>
<!-- 已上架货源加载更多 -->
<view class="load-more" wx:if="{{pagination.published.hasMore}}">
<view class="loading-text" wx:if="{{pagination.published.loading}}">
加载中...
</view>
<view class="load-more-text" wx:else bindtap="onReachPublishedBottom">
点击加载更多已上架货源
</view>
</view>
<view class="no-more" wx:if="{{!pagination.published.hasMore && publishedSupplies.length > 0}}">
没有更多已上架货源了
</view>
</block>
<view wx:else style="text-align: center; color: #999; font-size: 24rpx; padding: 30rpx 0;">
暂无已上架的货源
</view>
</block>
</view>
<!-- 审核中的货源 -->
<view style="margin-top: 30rpx; width: 100%;">
<view style="font-size: 28rpx; font-weight: bold; color: #1677ff; margin-bottom: 15rpx; display: flex; justify-content: space-between; align-items: center;">
<text>审核中的货源 ({{pendingSupplies.length}})</text>
<view bindtap="togglePendingExpand" style="width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center;">
<text wx:if="{{isPendingExpanded}}" style="color: #1677ff; font-size: 28rpx;">▼</text>
<text wx:else style="color: #1677ff; font-size: 28rpx;">▲</text>
</view>
</view>
<block wx:if="{{isPendingExpanded}}">
<block wx:if="{{pendingSupplies.length > 0}}">
<view wx:for="{{pendingSupplies}}" wx:key="id" class="card" style="width: 100%;">
<!-- 图片和信息1:1比例并排显示 -->
<view style="display: flex; width: 100%; border-radius: 8rpx; overflow: hidden; background-color: #f5f5f5;">
<!-- 左侧图片区域 50%宽度 -->
<view style="width: 50%; position: relative;">
<!-- 第一张图片 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 0}}" style="width: 100%; height: 100%;">
<image src="{{item.imageUrls[0]}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="0"></image>
</view>
<view wx:else style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999;">
<text>暂无图片</text>
</view>
<!-- 剩余图片可滑动区域 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 1}}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
<swiper
class="image-swiper"
style="width: 100%; height: 100%;"
current="{{item.currentImageIndex || 0}}"
bindchange="swiperChange"
data-id="{{item.id}}">
<block wx:for="{{item.imageUrls}}" wx:for-item="img" wx:for-index="idx" wx:key="idx">
<swiper-item>
<image src="{{img}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="{{idx}}"></image>
</swiper-item>
</block>
</swiper>
<!-- 显示页码指示器 -->
<view style="position: absolute; bottom: 10rpx; right: 10rpx; background-color: rgba(0,0,0,0.5); color: white; padding: 5rpx 10rpx; border-radius: 15rpx; font-size: 20rpx;">
{{(item.currentImageIndex || 0) + 1}}/{{item.imageUrls.length}}
</view>
</view>
</view>
<!-- 右侧信息区域 50%宽度 -->
<view style="width: 50%; padding: 15rpx; display: flex; flex-direction: column; justify-content: space-between; background-color: white; border-left: 1rpx solid #f0f0f0;">
<view>
<view style="font-size: 28rpx; font-weight: bold; word-break: break-word;">{{item.name}}
<view style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #1677ff; padding: 2rpx 8rpx; border-radius: 10rpx;">审核中</view>
</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">蛋黄: {{item.yolk || '无'}}</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">规格: {{item.spec || '无'}}</view>
<view style="color: #f5222d; font-size: 24rpx; margin-top: 8rpx;">件数: {{item.minOrder}}件</view>
<view style="color: #1677ff; font-size: 24rpx; margin-top: 8rpx;">斤重: {{item.grossWeight || ''}}斤</view>
<view style="color: #722ed1; font-size: 24rpx; margin-top: 8rpx;">地区: {{item.region || '未设置'}}</view>
<view style="font-size: 22rpx; color: #999; margin-top: 8rpx;">创建时间: {{item.formattedCreatedAt}}</view>
</view>
<!-- 按钮区域 -->
<view style="display: flex; justify-content: space-around; margin-top: 10rpx; gap: 10rpx;">
<button
style="background-color: #faad14; color: white; font-size: 22rpx; padding: 0 15rpx; line-height: 60rpx;"
bindtap="showEditSupply"
data-id="{{item.id}}"
>
编辑
</button>
<button
style="background-color: #f5222d; color: white; font-size: 22rpx; padding: 0 15rpx; line-height: 60rpx;"
bindtap="deleteSupply"
data-id="{{item.id}}"
>
删除
</button>
</view>
</view>
</view>
</view>
<!-- 审核中货源加载更多 -->
<view class="load-more" wx:if="{{pagination.pending.hasMore}}">
<view class="loading-text" wx:if="{{pagination.pending.loading}}">
加载中...
</view>
<view class="load-more-text" wx:else bindtap="onReachPendingBottom">
点击加载更多审核中货源
</view>
</view>
<view class="no-more" wx:if="{{!pagination.pending.hasMore && pendingSupplies.length > 0}}">
没有更多审核中货源了
</view>
</block>
<view wx:else style="text-align: center; color: #999; font-size: 24rpx; padding: 30rpx 0;">
暂无审核中的货源
</view>
</block>
</view>
<!-- 审核失败的货源 -->
<view style="margin-top: 30rpx; width: 100%;">
<view style="font-size: 28rpx; font-weight: bold; color: #f5222d; margin-bottom: 15rpx; display: flex; justify-content: space-between; align-items: center;">
<text>审核失败的货源 ({{rejectedSupplies.length}})</text>
<view bindtap="toggleRejectedExpand" style="width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center;">
<text wx:if="{{isRejectedExpanded}}" style="color: #f5222d; font-size: 28rpx;">▼</text>
<text wx:else style="color: #f5222d; font-size: 28rpx;">▲</text>
</view>
</view>
<block wx:if="{{isRejectedExpanded}}">
<block wx:if="{{rejectedSupplies.length > 0}}">
<view wx:for="{{rejectedSupplies}}" wx:key="id" class="card" style="width: 100%;">
<!-- 图片和信息1:1比例并排显示 -->
<view style="display: flex; width: 100%; border-radius: 8rpx; overflow: hidden; background-color: #f5f5f5;">
<!-- 左侧图片区域 50%宽度 -->
<view style="width: 50%; position: relative;">
<!-- 第一张图片 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 0}}" style="width: 100%; height: 100%;">
<image src="{{item.imageUrls[0]}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="0"></image>
</view>
<view wx:else style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999;">
<text>暂无图片</text>
</view>
<!-- 剩余图片可滑动区域 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 1}}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
<swiper
class="image-swiper"
style="width: 100%; height: 100%;"
current="{{item.currentImageIndex || 0}}"
bindchange="swiperChange"
data-id="{{item.id}}">
<block wx:for="{{item.imageUrls}}" wx:for-item="img" wx:for-index="idx" wx:key="idx">
<swiper-item>
<image src="{{img}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="{{idx}}"></image>
</swiper-item>
</block>
</swiper>
<!-- 显示页码指示器 -->
<view style="position: absolute; bottom: 10rpx; right: 10rpx; background-color: rgba(0,0,0,0.5); color: white; padding: 5rpx 10rpx; border-radius: 15rpx; font-size: 20rpx;">
{{(item.currentImageIndex || 0) + 1}}/{{item.imageUrls.length}}
</view>
</view>
</view>
<!-- 右侧信息区域 50%宽度 -->
<view style="width: 50%; padding: 15rpx; display: flex; flex-direction: column; justify-content: space-between; background-color: white; border-left: 1rpx solid #f0f0f0;">
<view>
<view style="font-size: 28rpx; font-weight: bold; word-break: break-word;">{{item.name}}
<view style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #f5222d; padding: 2rpx 8rpx; border-radius: 10rpx;">审核失败</view>
</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">蛋黄: {{item.yolk || '无'}}</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">规格: {{item.spec || '无'}}</view>
<view style="color: #f5222d; font-size: 24rpx; margin-top: 8rpx;">件数: {{item.minOrder}}件</view>
<view style="color: #1677ff; font-size: 24rpx; margin-top: 8rpx;">斤重: {{item.grossWeight || ''}}斤</view>
<view style="color: #722ed1; font-size: 24rpx; margin-top: 8rpx;">地区: {{item.region || '未设置'}}</view>
<view style="font-size: 22rpx; color: #999; margin-top: 8rpx;">创建时间: {{item.formattedCreatedAt}}</view>
<!-- 点击查看审核失败原因 -->
<view style="color: #f5222d; font-size: 24rpx; margin-top: 8rpx; text-decoration: underline;" bindtap="showRejectReason" data-id="{{item.id}}">
审核失败原因:点击查看
</view>
</view>
<!-- 按钮区域 -->
<view style="display: flex; justify-content: space-around; margin-top: 10rpx; gap: 10rpx;">
<button
style="background-color: #52c41a; color: white; font-size: 22rpx; padding: 0 15rpx; line-height: 60rpx;"
bindtap="preparePublishSupply"
data-id="{{item.id}}"
>
上架
</button>
<button
style="background-color: #f5222d; color: white; font-size: 22rpx; padding: 0 15rpx; line-height: 60rpx;"
bindtap="deleteSupply"
data-id="{{item.id}}"
>
删除
</button>
</view>
</view>
</view>
</view>
<!-- 审核失败货源加载更多 -->
<view class="load-more" wx:if="{{pagination.rejected.hasMore}}">
<view class="loading-text" wx:if="{{pagination.rejected.loading}}">
加载中...
</view>
<view class="load-more-text" wx:else bindtap="onReachRejectedBottom">
点击加载更多审核失败货源
</view>
</view>
<view class="no-more" wx:if="{{!pagination.rejected.hasMore && rejectedSupplies.length > 0}}">
没有更多审核失败货源了
</view>
</block>
<view wx:else style="text-align: center; color: #999; font-size: 24rpx; padding: 30rpx 0;">
暂无审核失败的货源
</view>
</block>
</view>
<!-- 草稿状态货源 -->
<view style="margin-top: 30rpx; width: 100%;">
<view style="font-size: 28rpx; font-weight: bold; color: #999; margin-bottom: 15rpx; display: flex; justify-content: space-between; align-items: center;">
<text>下架状态货源 ({{draftSupplies.length}})</text>
<view bindtap="toggleDraftExpand" style="width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center;">
<text wx:if="{{isDraftExpanded}}" style="color: #999; font-size: 28rpx;">▼</text>
<text wx:else style="color: #999; font-size: 28rpx;">▲</text>
</view>
</view>
<block wx:if="{{isDraftExpanded}}">
<block wx:if="{{draftSupplies.length > 0}}">
<view wx:for="{{draftSupplies}}" wx:key="id" class="card" style="width: 100%;">
<!-- 图片和信息1:1比例并排显示 -->
<view style="display: flex; width: 100%; border-radius: 8rpx; overflow: hidden; background-color: #f5f5f5;">
<!-- 左侧图片区域 50%宽度 -->
<view style="width: 50%; position: relative;">
<!-- 第一张图片 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 0}}" style="width: 100%; height: 100%;">
<image src="{{item.imageUrls[0]}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="0"></image>
</view>
<view wx:else style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999;">
<text>暂无图片</text>
</view>
<!-- 剩余图片可滑动区域 -->
<view wx:if="{{item.imageUrls && item.imageUrls.length > 1}}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
<swiper
class="image-swiper"
style="width: 100%; height: 100%;"
current="{{item.currentImageIndex || 0}}"
bindchange="swiperChange"
data-id="{{item.id}}">
<block wx:for="{{item.imageUrls}}" wx:for-item="img" wx:for-index="idx" wx:key="idx">
<swiper-item>
<image src="{{img}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{item.imageUrls}}" data-index="{{idx}}"></image>
</swiper-item>
</block>
</swiper>
<!-- 显示页码指示器 -->
<view style="position: absolute; bottom: 10rpx; right: 10rpx; background-color: rgba(0,0,0,0.5); color: white; padding: 5rpx 10rpx; border-radius: 15rpx; font-size: 20rpx;">
{{(item.currentImageIndex || 0) + 1}}/{{item.imageUrls.length}}
</view>
</view>
</view>
<!-- 右侧信息区域 50%宽度 -->
<view style="width: 50%; padding: 15rpx; display: flex; flex-direction: column; justify-content: space-between; background-color: white; border-left: 1rpx solid #f0f0f0;">
<view>
<view style="font-size: 28rpx; font-weight: bold; word-break: break-word;">{{item.name}}
<view wx:if="{{item.status === 'hidden'}}" style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #8c8c8c; padding: 2rpx 8rpx; border-radius: 10rpx;">已隐藏</view>
<view wx:elif="{{item.status === 'sold_out' || item.status === 'Undercarriage'}}" style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #d9d9d9; padding: 2rpx 8rpx; border-radius: 10rpx;">已下架</view>
<view wx:else style="display: inline-block; margin-left: 10rpx; font-size: 18rpx; color: #fff; background-color: #999; padding: 2rpx 8rpx; border-radius: 10rpx;">草稿</view>
</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">蛋黄: {{item.yolk || '无'}}</view>
<view style="font-size: 24rpx; color: #666; margin-top: 8rpx;">规格: {{item.spec || '无'}}</view>
<view style="color: #f5222d; font-size: 24rpx; margin-top: 8rpx;">件数: {{item.minOrder}}件</view>
<view style="color: #1677ff; font-size: 24rpx; margin-top: 8rpx;">斤重: {{item.grossWeight || ''}}斤</view>
<view style="color: #722ed1; font-size: 24rpx; margin-top: 8rpx;">地区: {{item.region || '未设置'}}</view>
<view style="font-size: 22rpx; color: #999; margin-top: 8rpx;">创建时间: {{item.formattedCreatedAt}}</view>
</view>
<!-- 按钮区域 -->
<view style="display: flex; justify-content: space-around; margin-top: 10rpx; gap: 10rpx; flex-wrap: wrap;">
<button
style="background-color: #1677ff; color: white; font-size: 22rpx; padding: 0 12rpx; line-height: 56rpx;"
bindtap="preparePublishSupply"
data-id="{{item.id}}"
>
上架
</button>
<!-- <button
style="background-color: #faad14; color: white; font-size: 22rpx; padding: 0 12rpx; line-height: 56rpx;"
bindtap="showEditSupply"
data-id="{{item.id}}"
>
编辑
</button> -->
<button
style="background-color: #f5222d; color: white; font-size: 22rpx; padding: 0 12rpx; line-height: 56rpx;"
bindtap="deleteSupply"
data-id="{{item.id}}"
>
删除
</button>
</view>
</view>
</view>
</view>
<!-- 下架状态货源加载更多 -->
<view class="load-more" wx:if="{{pagination.draft.hasMore}}">
<view class="loading-text" wx:if="{{pagination.draft.loading}}">
加载中...
</view>
<view class="load-more-text" wx:else bindtap="onReachDraftBottom">
点击加载更多下架状态货源
</view>
</view>
<view class="no-more" wx:if="{{!pagination.draft.hasMore && draftSupplies.length > 0}}">
没有更多下架状态货源了
</view>
</block>
<view wx:else style="text-align: center; color: #999; font-size: 24rpx; padding: 30rpx 0;">
暂无下架状态的货源
</view>
</block>
</view>
<!-- 创建货源弹窗 -->
<view class="modal" wx:if="{{showModal}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 999;" catchtouchmove="true" bindtouchstart="onModalTouchStart" bindtouchmove="onModalTouchMove">
<view class="modal-content" style="width: 92%; max-width: 600rpx; background: white; padding: 40rpx; border-radius: 20rpx; max-height: 85vh; position: relative; box-shadow: 0 10rpx 40rpx rgba(0,0,0,0.15); transform: translateZ(0); -webkit-transform: translateZ(0);">
<!-- 固定的关闭按钮 -->
<view style="position: absolute; top: 20rpx; right: 20rpx; background-color: #f5f5f5; color: #666; width: 60rpx; height: 60rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36rpx; z-index: 10;" bindtap="hideModal">×</view>
<scroll-view scroll-y="true" style="height: 950rpx; overflow-y: scroll; -webkit-overflow-scrolling: touch; transform: translateZ(0); -webkit-transform: translateZ(0);" catchtouchmove="true" bindtouchstart="onModalTouchStart" bindtouchmove="onModalTouchMove">
<view class="title" style="text-align: center; font-size: 36rpx; font-weight: bold; color: #333; margin-bottom: 30rpx; margin-top: 10rpx;">创建货源</view>
<!-- 照片上传区域 -->
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-top: 10rpx;">商品图片</view>
<view class="upload-area" style="width: 100%; margin: 0 auto; margin-bottom: 30rpx; border: 1rpx dashed #ddd; border-radius: 12rpx; padding: 24rpx;">
<view style="display: flex; flex-wrap: wrap;">
<!-- 已上传的图片 -->
<view wx:for="{{newSupply.imageUrls}}" wx:key="index" style="position: relative; width: 160rpx; height: 160rpx; margin: 10rpx; border-radius: 12rpx; overflow: hidden; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);">
<image src="{{item}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{newSupply.imageUrls}}" data-index="{{index}}"></image>
<view class="delete-icon" style="position: absolute; top: 8rpx; right: 8rpx; background-color: rgba(0,0,0,0.6); color: white; width: 44rpx; height: 44rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28rpx;" bindtap="deleteImage" data-index="{{index}}" data-type="new">×</view>
</view>
<!-- 上传按钮 -->
<view wx:if="{{newSupply.imageUrls.length < 5}}" style="width: 160rpx; height: 160rpx; margin: 10rpx; border: 2rpx dashed #1677ff; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; background-color: #f0f8ff;" bindtap="chooseImage" data-type="new">
<text style="font-size: 60rpx; color: #1677ff;">+</text>
</view>
</view>
<view style="font-size: 22rpx; color: #999; margin-top: 16rpx; text-align: center;">最多上传5张图片</view>
</view>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">商品名称</view>
<view
bindtap="openNameSelectModal"
style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block; background: white; position: relative;">
<view style="display: flex; justify-content: space-between; align-items: center;">
<text>{{newSupply.name || '请选择商品名称'}}</text>
<text style="color: #999;">▼</text>
</view>
</view>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">蛋黄</view>
<view bindtap="openYolkSelectModal" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block; background: white; position: relative;">
<view style="display: flex; justify-content: space-between; align-items: center;">
<text>{{newSupply.yolk || '请选择蛋黄类型'}}</text>
<text style="color: #999;">▼</text>
</view>
</view>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">规格</view>
<!-- 修改为可点击的视图,点击后打开自定义弹窗 -->
<view bindtap="onSpecChange" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block; background: white; position: relative; z-index: 1;">
<view style="display: flex; justify-content: space-between; align-items: center;">
<text>{{newSupply.spec || '请选择规格'}}</text>
<text style="color: #999;">▼</text>
</view>
</view>
<!-- 搜索功能已移至弹窗内 -->
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">价格</view>
<input class="input" type="text" placeholder="请输入价格" bindinput="onInput" data-field="price" value="{{newSupply.price}}" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block;" placeholder-style="font-size: 24rpx; color: #999; text-align: left;" catchtouchmove="true" bindtouchstart="onInputTouchStart" bindtouchmove="onInputTouchMove"></input>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">件数</view>
<input class="input" type="number" placeholder="请输入件数" bindinput="onInput" data-field="minOrder" value="{{newSupply.minOrder}}" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block;" placeholder-style="font-size: 24rpx; color: #999; text-align: left;" catchtouchmove="true" bindtouchstart="onInputTouchStart" bindtouchmove="onInputTouchMove"></input>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">斤重</view>
<input class="input" type="text" placeholder="请输入斤重" bindinput="onInput" data-field="grossWeight" value="{{newSupply.grossWeight || ''}}" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block;" placeholder-style="font-size: 24rpx; color: #999; text-align: left;" catchtouchmove="true" bindtouchstart="onInputTouchStart" bindtouchmove="onInputTouchMove"></input>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">地区</view>
<input class="input" placeholder="请输入地区" bindinput="onInput" data-field="region" value="{{newSupply.region}}" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block;" placeholder-style="font-size: 24rpx; color: #999; text-align: left;" catchtouchmove="true" bindtouchstart="onInputTouchStart" bindtouchmove="onInputTouchMove"></input>
<view style="display: flex; justify-content: space-between; margin-top: 20rpx; margin-bottom: 20rpx; gap: 20rpx;">
<button bindtap="hideModal" style="flex: 1; height: 90rpx; line-height: 90rpx; background-color: #f5f5f5; color: #666; font-size: 30rpx; border-radius: 12rpx; margin: 0; display: flex; align-items: center; justify-content: center;">取消</button>
<button bindtap="addSupply" style="flex: 1; height: 90rpx; line-height: 90rpx; background-color: #07c160; color: white; font-size: 30rpx; border-radius: 12rpx; margin: 0; display: flex; align-items: center; justify-content: center;">创建</button>
</view>
</scroll-view>
</view>
</view>
<!-- 编辑货源弹窗 -->
<view class="modal" wx:if="{{showEditModal}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 999;" catchtouchmove="true" bindtouchstart="onModalTouchStart" bindtouchmove="onModalTouchMove">
<view class="modal-content" style="width: 92%; max-width: 600rpx; background: white; padding: 40rpx; border-radius: 20rpx; max-height: 85vh; position: relative; box-shadow: 0 10rpx 40rpx rgba(0,0,0,0.15); transform: translateZ(0); -webkit-transform: translateZ(0);">
<!-- 固定的关闭按钮 -->
<view style="position: absolute; top: 20rpx; right: 20rpx; background-color: #f5f5f5; color: #666; width: 60rpx; height: 60rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36rpx; z-index: 10;" bindtap="hideEditModal">×</view>
<scroll-view scroll-y="true" style="height: 950rpx; overflow-y: scroll; -webkit-overflow-scrolling: touch; transform: translateZ(0); -webkit-transform: translateZ(0);" catchtouchmove="true" bindtouchstart="onModalTouchStart" bindtouchmove="onModalTouchMove">
<view class="title" style="text-align: center; font-size: 36rpx; font-weight: bold; color: #333; margin-bottom: 30rpx; margin-top: 10rpx;">编辑货源</view>
<!-- 照片上传区域 -->
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-top: 10rpx;">商品图片</view>
<view class="upload-area" style="width: 100%; margin: 0 auto; margin-bottom: 30rpx; border: 1rpx dashed #ddd; border-radius: 12rpx; padding: 24rpx;">
<view style="display: flex; flex-wrap: wrap;">
<!-- 已上传的图片 -->
<view wx:for="{{editSupply.imageUrls}}" wx:key="index" style="position: relative; width: 160rpx; height: 160rpx; margin: 10rpx; border-radius: 12rpx; overflow: hidden; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);">
<image src="{{item}}" mode="aspectFill" style="width: 100%; height: 100%;" bindtap="previewImage" data-urls="{{editSupply.imageUrls}}" data-index="{{index}}"></image>
<view class="delete-icon" style="position: absolute; top: 8rpx; right: 8rpx; background-color: rgba(0,0,0,0.6); color: white; width: 44rpx; height: 44rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28rpx;" bindtap="deleteImage" data-index="{{index}}" data-type="edit">×</view>
</view>
<!-- 上传按钮 -->
<view wx:if="{{editSupply.imageUrls.length < 5}}" style="width: 160rpx; height: 160rpx; margin: 10rpx; border: 2rpx dashed #1677ff; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; background-color: #f0f8ff;" bindtap="chooseImage" data-type="edit">
<text style="font-size: 60rpx; color: #1677ff;">+</text>
</view>
</view>
<view style="font-size: 22rpx; color: #999; margin-top: 16rpx; text-align: center;">最多上传5张图片</view>
</view>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">商品名称</view>
<view
bindtap="openNameSelectModal"
style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block; background: white; position: relative;">
<view style="display: flex; justify-content: space-between; align-items: center;">
<text>{{editSupply.name || '请选择商品名称'}}</text>
<text style="color: #999;">▼</text>
</view>
</view>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">蛋黄</view>
<view bindtap="openYolkSelectModal" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block; background: white; position: relative;">
<view style="display: flex; justify-content: space-between; align-items: center;">
<text>{{editSupply.yolk || '请选择蛋黄类型'}}</text>
<text style="color: #999;">▼</text>
</view>
</view>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">规格</view>
<!-- 修改为可点击的视图,点击后打开自定义弹窗 -->
<view bindtap="onEditSpecChange" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block; background: white; position: relative; z-index: 1;">
<view style="display: flex; justify-content: space-between; align-items: center;">
<text>{{editSupply.spec || '请选择规格'}}</text>
<text style="color: #999;">▼</text>
</view>
</view>
<!-- 搜索功能已移至弹窗内 -->
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">价格</view>
<input class="input" type="text" placeholder="请输入价格" bindinput="onEditInput" data-field="price" value="{{editSupply.price}}" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block;" placeholder-style="font-size: 24rpx; color: #999; text-align: left;" catchtouchmove="true" bindtouchstart="onInputTouchStart" bindtouchmove="onInputTouchMove"></input>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">件数</view>
<input class="input" type="number" placeholder="请输入件数" bindinput="onEditInput" data-field="minOrder" value="{{editSupply.minOrder}}" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block;" placeholder-style="font-size: 24rpx; color: #999; text-align: left;" catchtouchmove="true" bindtouchstart="onInputTouchStart" bindtouchmove="onInputTouchMove"></input>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">斤重</view>
<input class="input" type="text" placeholder="请输入斤重" bindinput="onEditInput" data-field="grossWeight" value="{{editSupply.grossWeight || ''}}" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block;" placeholder-style="font-size: 24rpx; color: #999; text-align: left;" catchtouchmove="true" bindtouchstart="onInputTouchStart" bindtouchmove="onInputTouchMove"></input>
<view style="font-size: 28rpx; font-weight: 500; color: #333; margin-bottom: 12rpx; margin-left: 10rpx;">地区</view>
<input class="input" placeholder="请输入地区" bindinput="onEditInput" data-field="region" value="{{editSupply.region}}" style="width: 100%; height: 90rpx; line-height: 90rpx; padding: 0 24rpx; font-size: 30rpx; border: 2rpx solid #eee; border-radius: 12rpx; box-sizing: border-box; margin: 0 auto 30rpx; display: block;" placeholder-style="font-size: 24rpx; color: #999; text-align: left;" catchtouchmove="true" bindtouchstart="onInputTouchStart" bindtouchmove="onInputTouchMove"></input>
<view style="display: flex; justify-content: space-between; margin-top: 20rpx; margin-bottom: 20rpx; gap: 20rpx;">
<button bindtap="hideEditModal" style="flex: 1; height: 90rpx; line-height: 90rpx; background-color: #f5f5f5; color: #666; font-size: 30rpx; border-radius: 12rpx; margin: 0; display: flex; align-items: center; justify-content: center;">取消</button>
<button bindtap="saveEdit" style="flex: 1; height: 90rpx; line-height: 90rpx; background-color: #07c160; color: white; font-size: 30rpx; border-radius: 12rpx; margin: 0; display: flex; align-items: center; justify-content: center;">提交</button>
</view>
</scroll-view>
</view>
</view>
<!-- 图片预览弹窗 -->
<view class="image-preview-mask" wx:if="{{showImagePreview}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.9); display: flex; justify-content: center; align-items: center; z-index: 9999;" catchtouchmove="true" bindtap="closeImagePreview">
<view style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;">
<swiper
style="width: 100%; height: 100%;"
current="{{previewImageIndex}}"
bindchange="onPreviewImageChange"
indicator-dots="true"
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#fff">
<block wx:for="{{previewImageUrls}}" wx:key="*this">
<swiper-item>
<image
src="{{item}}"
mode="aspectFit"
style="width: 100%; height: 100%; transform: scale({{scale}}) translate({{offsetX}}px, {{offsetY}}px); transform-origin: center; transition: transform 0.1s;"
bindtap="handleImageTap"
bindtouchstart="handleTouchStart"
bindtouchmove="handleTouchMove"
bindtouchend="handleTouchEnd"
bindload="onPreviewImageLoad"
></image>
</swiper-item>
</block>
</swiper>
<view style="position: absolute; top: 40rpx; right: 40rpx; color: white; font-size: 40rpx;">
<text bindtap="closeImagePreview" style="background: rgba(0,0,0,0.5); padding: 10rpx 20rpx; border-radius: 50%;">×</text>
</view>
</view>
</view>
<!-- 审核失败原因弹窗 -->
<view class="reject-reason-modal" wx:if="{{showRejectReasonModal}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 9999;" catchtouchmove="true">
<view style="width: 80%; background: white; border-radius: 16rpx; overflow: hidden;">
<!-- 弹窗标题和关闭按钮 -->
<view style="padding: 30rpx; border-bottom: 1rpx solid #eee; display: flex; justify-content: space-between; align-items: center;">
<text style="font-size: 32rpx; font-weight: bold;">审核失败原因</text>
<view style="width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; font-size: 40rpx; color: #999;" bindtap="closeRejectReasonModal">×</view>
</view>
<!-- 失败原因内容 -->
<view style="padding: 30rpx;">
<view style="min-height: 200rpx; font-size: 28rpx; line-height: 48rpx; color: #666; white-space: pre-wrap; word-break: break-word;">{{rejectReason}}</view>
</view>
<!-- 操作按钮 -->
<view style="display: flex; border-top: 1rpx solid #eee;">
<!-- <button style="flex: 1; background-color: #faad14; color: white; font-size: 28rpx; margin: 0; border-radius: 0; border-right: 1rpx solid #eee;" bindtap="editRejectedSupply">编辑</button> -->
<button style="flex: 1; background-color: #52c41a; color: white; font-size: 28rpx; margin: 0; border-radius: 0;" bindtap="resubmitRejectedSupply">重新提交</button>
</view>
</view>
</view>
<!-- 蛋黄选择弹窗 - 白色样式 -->
<view class="custom-select-modal" wx:if="{{showYolkSelectModal}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; z-index: 9999;" catchtouchmove="true">
<view style="position: fixed; bottom: 0; left: 0; right: 0; background: white; border-radius: 20rpx 20rpx 0 0; max-height: 80vh;">
<!-- 顶部操作栏:取消和确定按钮 -->
<view style="padding: 20rpx; display: flex; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #eee;">
<view bindtap="closeYolkSelectModal" style="font-size: 32rpx; color: #333; padding: 10rpx 20rpx;">取消</view>
<view bindtap="confirmYolkSelection" style="font-size: 32rpx; color: #07c160; padding: 10rpx 20rpx;">确定</view>
</view>
<!-- 蛋黄列表 -->
<scroll-view
scroll-y="true"
style="max-height: 60vh; padding: 0; -webkit-overflow-scrolling: touch;"
enable-back-to-top="false"
>
<view
wx:for="{{yolkOptions}}"
wx:key="index"
class="select-item {{selectedYolkIndex === index ? 'selected' : ''}}"
bindtap="onYolkSelect"
data-index="{{index}}"
style="padding: 32rpx 40rpx; border-bottom: 1rpx solid #f0f0f0; font-size: 32rpx; color: {{selectedYolkIndex === index ? '#07c160' : '#131413'}}; text-align: center;"
>
{{item}}
</view>
</scroll-view>
</view>
</view>
<!-- 商品名称选择弹窗 - 白色样式 -->
<view class="custom-select-modal" wx:if="{{showNameSelectModal}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; z-index: 9999;" catchtouchmove="true">
<view style="position: fixed; bottom: 0; left: 0; right: 0; background: white; border-radius: 20rpx 20rpx 0 0; max-height: 80vh;">
<!-- 顶部操作栏:取消和确定按钮 -->
<view style="padding: 20rpx; display: flex; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #eee;">
<view bindtap="closeNameSelectModal" style="font-size: 32rpx; color: #333; padding: 10rpx 20rpx;">取消</view>
<view bindtap="confirmNameSelection" style="font-size: 32rpx; color: #07c160; padding: 10rpx 20rpx;">确定</view>
</view>
<!-- 商品名称列表 -->
<scroll-view
scroll-y="true"
style="max-height: 60vh; padding: 0; -webkit-overflow-scrolling: touch;"
enable-back-to-top="false"
>
<view
wx:for="{{productNameOptions}}"
wx:key="index"
class="select-item {{selectedNameIndex === index ? 'selected' : ''}}"
bindtap="onNameSelect"
data-index="{{index}}"
style="padding: 32rpx 40rpx; border-bottom: 1rpx solid #f0f0f0; font-size: 32rpx; color: {{selectedNameIndex === index ? '#07c160' : '#131413'}}; text-align: center;"
>
{{item}}
</view>
</scroll-view>
</view>
</view>
<!-- 自定义规格选择弹窗 - 适配原生风格 -->
<view class="spec-select-modal" wx:if="{{showSpecSelectModal}}" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; z-index: 9999;" catchtouchmove="true">
<view style="position: fixed; bottom: 0; left: 0; right: 0; background: white; border-radius: 20rpx 20rpx 0 0; max-height: 80vh;">
<!-- 顶部操作栏:取消和确定按钮 -->
<view style="padding: 20rpx; display: flex; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #eee;">
<view bindtap="closeSpecSelectModal" style="font-size: 32rpx; color: #333; padding: 10rpx 20rpx;">取消</view>
<view bindtap="confirmSpecSelection" style="font-size: 32rpx; color: #07c160; padding: 10rpx 20rpx;">确定</view>
</view>
<!-- 搜索框区域 -->
<view style="padding: 20rpx;">
<view style="position: relative; background: #f5f5f5; border-radius: 40rpx; padding: 0 30rpx;">
<input
type="text"
placeholder="搜索规格"
value="{{modalSpecSearchKeyword}}"
bindinput="onModalSpecSearchInput"
confirm-type="search"
style="width: 100%; height: 70rpx; line-height: 70rpx; font-size: 28rpx; background: transparent;"
/>
<view
wx:if="{{modalSpecSearchKeyword}}"
bindtap="clearModalSpecSearch"
style="position: absolute; right: 30rpx; top: 50%; transform: translateY(-50%); color: #999;"
>
</view>
</view>
</view>
<!-- 规格列表 -->
<scroll-view
scroll-y="true"
style="max-height: 60vh; padding: 0; -webkit-overflow-scrolling: touch;"
enable-back-to-top="false"
>
<view
wx:for="{{filteredModalSpecOptions}}"
wx:key="index"
class="spec-item {{selectedModalSpecIndex === index ? 'selected' : ''}}"
bindtap="onModalSpecSelect"
data-index="{{index}}"
style="padding: 32rpx 40rpx; border-bottom: 1rpx solid #f0f0f0; font-size: 32rpx; color: {{selectedModalSpecIndex === index ? '#07c160' : '#131413'}}; text-align: center;"
>
{{item}}
</view>
</scroll-view>
</view>
</view>
<!-- 未授权登录提示弹窗 -->
<view wx:if="{{showAuthModal}}" class="auth-modal-overlay">
<view class="auth-modal-container">
<view class="auth-modal-title">
<text>提示</text>
</view>
<view class="auth-modal-content">
<text>您还没有授权登录</text>
</view>
<view class="auth-modal-buttons">
<button class="auth-primary-button" bindtap="showOneKeyLogin">
一键登录
</button>
<button class="auth-cancel-button" bindtap="closeAuthModal">取消</button>
</view>
</view>
</view>
<!-- 一键登录弹窗 -->
<view wx:if="{{showOneKeyLoginModal}}" class="auth-modal-overlay">
<view class="auth-modal-container">
<view class="auth-modal-title">
<text>授权登录</text>
</view>
<view class="auth-modal-content">
<text>请授权获取您的手机号用于登录</text>
</view>
<view class="auth-modal-buttons">
<button class="auth-primary-button" open-type="getPhoneNumber" bind:getphonenumber="onGetPhoneNumber">
授权获取手机号
</button>
<button class="auth-cancel-button" bindtap="closeOneKeyLoginModal">取消</button>
</view>
</view>
</view>
<!-- 用户信息填写弹窗 -->
<view wx:if="{{showUserInfoForm}}" class="auth-modal-overlay">
<view class="auth-modal-container">
<view class="auth-modal-title">
<text>完善个人信息</text>
</view>
<!-- 头像选择 -->
<view class="auth-avatar-section">
<button class="auth-avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image class="auth-avatar" src="{{avatarUrl}}"></image>
</button>
</view>
<!-- 昵称输入 -->
<form bindsubmit="getUserName">
<view class="auth-form-group">
<view class="auth-form-label">昵称</view>
<input placeholder="请输入昵称" type="nickname" name="nickname" maxlength="32" class="auth-form-input"></input>
</view>
<!-- 提交按钮 -->
<view class="auth-form-actions">
<button form-type="submit" class="auth-confirm-button">确定</button>
</view>
</form>
<!-- 取消按钮 -->
<view class="auth-modal-buttons">
<button class="auth-cancel-button" bindtap="cancelUserInfoForm">取消</button>
</view>
</view>
</view>
</view>

295
pages/seller/index.wxss

@ -0,0 +1,295 @@
/* pages/seller/index.wxss */
/* 立体玻璃质感按钮基础样式 */
.glass-btn {
position: relative;
overflow: hidden;
border: none;
padding: 28rpx 40rpx;
font-size: 32rpx;
font-weight: 600;
border-radius: 16rpx;
box-shadow:
0 8rpx 24rpx rgba(0, 0, 0, 0.15),
0 0 0 1rpx rgba(255, 255, 255, 0.3) inset,
0 1rpx 0 rgba(255, 255, 255, 0.2) inset;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10rpx);
-webkit-backdrop-filter: blur(10rpx);
white-space: nowrap;
width: auto;
min-width: 80%;
text-align: center;
}
/* 按压效果 */
.glass-btn:active {
transform: translateY(2rpx);
box-shadow:
0 4rpx 12rpx rgba(0, 0, 0, 0.1),
0 0 0 1rpx rgba(255, 255, 255, 0.2) inset,
0 1rpx 0 rgba(255, 255, 255, 0.1) inset;
}
/* 主按钮 - 蓝色玻璃效果 */
.primary-glass-btn {
background: rgba(22, 119, 255, 0.7);
color: white;
box-shadow:
0 8rpx 24rpx rgba(22, 119, 255, 0.3),
0 0 0 1rpx rgba(255, 255, 255, 0.3) inset,
0 1rpx 0 rgba(255, 255, 255, 0.2) inset;
}
/* 联系客服按钮样式 */
.customer-service-btn {
background: #f0f0f0;
color: black;
font-size: 30rpx;
width: 70rpx;
height: 80rpx;
line-height: 70rpx;
padding: 0;
border-radius: 500rpx;
border: 1rpx solid #d9d9d9;
box-shadow:
0 4rpx 16rpx rgba(0, 0, 0, 0.1),
0 1rpx 0 rgba(255, 255, 255, 0.5) inset;
transition: all 0.3s ease;
white-space: nowrap;
text-align: center;
}
.customer-service-btn:active {
transform: translateY(2rpx);
background: #f5f5f5;
box-shadow:
0 2rpx 8rpx rgba(0, 0, 0, 0.08),
0 1rpx 0 rgba(255, 255, 255, 0.3) inset;
}
/* 登录授权弹窗样式 - 专门用于登录相关弹窗 */
.auth-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.auth-modal-container {
background-color: white;
border-radius: 16rpx;
width: 80%;
padding: 40rpx;
}
.auth-modal-title {
text-align: center;
margin-bottom: 30rpx;
}
.auth-modal-title text {
font-size: 36rpx;
font-weight: bold;
}
.auth-modal-content {
text-align: center;
margin-bottom: 40rpx;
color: #666;
}
.auth-modal-content text {
font-size: 32rpx;
}
.auth-modal-buttons {
text-align: center;
}
.auth-primary-button {
background-color: #1677ff;
color: white;
width: 100%;
border-radius: 8rpx;
margin-bottom: 20rpx;
border: none;
}
.auth-cancel-button {
background: none;
color: #666;
border: none;
}
/* 头像选择样式 */
.auth-avatar-section {
text-align: center;
margin-bottom: 40rpx;
}
.auth-avatar-wrapper {
padding: 0;
background: none;
border: none;
}
.auth-avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
}
/* 表单样式 */
.auth-form-group {
margin-bottom: 30rpx;
}
.auth-form-label {
font-size: 28rpx;
margin-bottom: 10rpx;
display: block;
}
.auth-form-input {
border: 1rpx solid #eee;
border-radius: 8rpx;
padding: 20rpx;
width: 100%;
max-width: 100%;
box-sizing: border-box;
font-size: 28rpx;
}
.auth-form-actions {
text-align: center;
margin-top: 40rpx;
}
.auth-confirm-button {
background-color: #07c160;
color: white;
width: 100%;
border-radius: 8rpx;
border: none;
}
/* 加载更多样式 */
.load-more {
text-align: center;
padding: 30rpx;
color: #666;
background-color: #f9f9f9;
border-radius: 8rpx;
margin: 20rpx 0;
}
.loading-text {
color: #999;
font-size: 26rpx;
}
.load-more-text {
color: #1677ff;
font-size: 26rpx;
padding: 15rpx 30rpx;
border: 1rpx solid #1677ff;
border-radius: 8rpx;
display: inline-block;
background-color: white;
}
.no-more {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 26rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
margin: 20rpx 0;
}
/* 页面滚动锁定样式 */
.page-scroll-lock {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
height: 100vh;
/* iOS设备特殊锁定机制 */
-webkit-overflow-scrolling: auto;
touch-action: none;
pointer-events: none;
/* iOS设备硬件加速处理 */
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
/* iOS设备子元素交互修复 */
.page-scroll-lock view,
.page-scroll-lock text,
.page-scroll-lock image,
.page-scroll-lock button,
.page-scroll-lock input,
.page-scroll-lock textarea,
.page-scroll-lock scroll-view,
.page-scroll-lock swiper,
.page-scroll-lock navigator,
.page-scroll-lock picker,
.page-scroll-lock slider,
.page-scroll-lock switch {
pointer-events: auto;
}
/* 弹窗输入框防抖动样式 */
.modal-content .input {
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
tap-highlight-color: transparent;
outline: none;
-webkit-user-select: text;
user-select: text;
-webkit-touch-callout: none;
touch-callout: none;
}
/* 弹窗容器硬件加速 */
.modal-content {
will-change: transform;
-webkit-will-change: transform;
}
/* 输入框容器稳定性增强 */
.modal-content input,
.modal-content textarea {
transform: translateZ(0);
-webkit-transform: translateZ(0);
perspective: 1000px;
-webkit-perspective: 1000px;
}
/* iOS输入框防抖优化 */
.modal-content .input {
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
-webkit-touch-callout: none;
-webkit-user-select: text;
user-select: text;
-webkit-transform: translateZ(0);
transform: translateZ(0);
will-change: transform;
}

1164
pages/settlement/index.js

File diff suppressed because it is too large

4
pages/settlement/index.json

@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "立即入驻"
}

367
pages/settlement/index.wxml

@ -0,0 +1,367 @@
<view class="container">
<!-- 内容区域 -->
<view class="content">
<!-- 引导页 -->
<view class="guide-page" wx:if="{{showGuidePage}}">
<view class="guide-content">
<view class="guide-title">成为供应商</view>
<view class="guide-description">完成入驻后即可发布货源,开展鸡蛋贸易</view>
<button class="guide-button btn btn-primary" bindtap="startSettlement">立即入驻</button>
</view>
</view>
<!-- 实际入驻流程内容 -->
<view wx:if="{{!showGuidePage}}">
<!-- 步骤指示器 -->
<view class="step-indicator">
<view class="step {{currentStep >= 0 ? 'active' : ''}}">
<view class="step-circle">1</view>
<text>选择身份</text>
</view>
<view class="step-line"></view>
<view class="step {{currentStep >= 1 ? 'active' : ''}}">
<view class="step-circle">2</view>
<text>基本信息</text>
</view>
<view class="step-line"></view>
<view class="step {{currentStep >= 2 ? 'active' : ''}}">
<view class="step-circle">3</view>
<text>上传资料</text>
</view>
<view class="step-line"></view>
<view class="step {{currentStep >= 3 ? 'active' : ''}}">
<view class="step-circle">4</view>
<text>审核状态</text>
</view>
</view>
<!-- 身份选择页面 -->
<view class="page" wx:if="{{currentStep === 0}}">
<view class="section">
<view class="section-title required">请选择您的身份</view>
<view class="identity-options">
<view class="identity-option {{collaborationid === 'chicken' ? 'selected' : ''}}"
data-identity="chicken" bindtap="selectIdentity">
<view class="identity-icon">
<text class="icon">🐔</text>
</view>
<view class="identity-text">
<view class="identity-title">鸡场</view>
<view class="identity-desc">养殖场主、生产商身份</view>
</view>
</view>
<view class="identity-option {{collaborationid === 'trader' ? 'selected' : ''}}"
data-identity="trader" bindtap="selectIdentity">
<view class="identity-icon">
<text class="icon">💰</text>
</view>
<view class="identity-text">
<view class="identity-title">贸易商</view>
<view class="identity-desc">经销商、批发商身份</view>
</view>
</view>
</view>
<view class="error-message" wx:if="{{showIdentityError}}">请选择身份</view>
</view>
<button class="btn btn-primary" bindtap="nextStep">下一步</button>
</view>
<!-- 基本信息页面 -->
<view class="page" wx:if="{{currentStep === 1}}">
<view class="basic-info-form">
<!-- 公司名称 -->
<view class="form-item">
<view class="form-header">
<text class="form-label required">公司名称</text>
</view>
<view class="input-wrapper">
<input
class="form-input {{showCompanyNameError ? 'error' : ''}}"
placeholder="请输入公司名称"
placeholder-class="form-input-placeholder"
value="{{company}}"
bindinput="onCompanyNameInput"
bindblur="onCompanyNameBlur"
bindfocus="onCompanyNameFocus"
maxlength="50"
type="text"
confirm-type="next"
cursor-spacing="10"
adjust-position="{{true}}"
hold-keyboard="{{false}}"
/>
<text class="input-icon" wx:if="{{company && !showCompanyNameError}}">✓</text>
</view>
<text class="error-message" wx:if="{{showCompanyNameError}}">{{companyNameError || '请输入公司名称'}}</text>
</view>
<!-- 地址信息 -->
<view class="form-item">
<view class="form-header">
<text class="form-label required">所在地区</text>
</view>
<picker
mode="region"
value="{{[province, city, district]}}"
bindchange="onRegionChange"
class="region-picker {{showRegionError ? 'error' : ''}} {{!(province || city || district) ? 'placeholder' : ''}}"
>
<view class="picker-content">
<text>{{province || city || district ? province + ' ' + city + ' ' + district : '请选择省市区'}}</text>
<view class="picker-arrow">▼</view>
</view>
</picker>
<text class="error-message" wx:if="{{showRegionError}}">{{regionError || '请选择所在地区'}}</text>
</view>
<!-- 详细地址 -->
<view class="form-item">
<view class="form-header">
<text class="form-label">详细地址</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
placeholder="请输入详细地址(选填)"
placeholder-class="form-input-placeholder"
value="{{detailedaddress}}"
bindinput="onDetailAddressInput"
bindfocus="onDetailAddressFocus"
maxlength="100"
type="text"
confirm-type="done"
cursor-spacing="10"
adjust-position="{{true}}"
hold-keyboard="{{false}}"
/>
</view>
</view>
<!-- 合作模式 -->
<view class="form-item">
<view class="form-header">
<text class="form-label required">合作模式</text>
</view>
<view class="cooperation-options">
<view
class="cooperation-option {{cooperation === '货源委托' ? 'active' : ''}}"
bindtap="selectCooperation"
data-value="货源委托"
>
<view class="cooperation-icon"></view>
<text class="cooperation-text">货源委托</text>
</view>
<view
class="cooperation-option {{cooperation === '自主定价销售' ? 'active' : ''}}"
bindtap="selectCooperation"
data-value="自主定价销售"
>
<view class="cooperation-icon"></view>
<text class="cooperation-text">自主定价销售</text>
</view>
<view
class="cooperation-option {{cooperation === '区域包场合作' ? 'active' : ''}}"
bindtap="selectCooperation"
data-value="区域包场合作"
>
<view class="cooperation-icon"></view>
<text class="cooperation-text">区域包场合作</text>
</view>
<view
class="cooperation-option {{cooperation === '其他' ? 'active' : ''}}"
bindtap="selectCooperation"
data-value="其他"
>
<view class="cooperation-icon"></view>
<text class="cooperation-text">其他</text>
</view>
</view>
<text class="error-message" wx:if="{{showCooperationError}}">{{cooperationError || '请选择合作模式'}}</text>
</view>
</view>
<!-- 底部按钮 -->
<view class="button-group">
<button class="btn btn-secondary" bindtap="prevStep">返回上一步</button>
<button class="btn btn-primary" bindtap="nextStep">下一步</button>
</view>
</view>
<!-- 上传资料页面 -->
<view class="page" wx:if="{{currentStep === 2}}">
<!-- 营业执照上传 -->
<view class="section">
<view class="section-title">{{collaborationid === 'chicken' ? '鸡场营业执照(选填)' : '贸易商营业执照(选填)'}}</view>
<view class="upload-area" bindtap="uploadBusinessLicense">
<view class="upload-icon">+</view>
<view class="upload-text">点击上传营业执照</view>
<view class="upload-tip">支持jpg、png格式,大小不超过5M</view>
</view>
<view wx:if="{{businesslicenseurl}}">
<view class="uploaded-file">
<view class="file-icon">📄</view>
<view class="file-name">{{businesslicenseurl.name}}</view>
<view class="file-delete" bindtap="deleteBusinessLicense">删除</view>
</view>
</view>
</view>
<!-- 鸡场特有字段 -->
<view class="section" wx:if="{{collaborationid === 'chicken'}}">
<view class="section-title">动物检疫合格证明(选填)</view>
<view class="upload-area" bindtap="uploadAnimalQuarantine">
<view class="upload-icon">+</view>
<view class="upload-text">点击上传动物检疫合格证明</view>
<view class="upload-tip">支持jpg、png格式,大小不超过5M</view>
</view>
<view wx:if="{{proofurl}}">
<view class="uploaded-file">
<view class="file-icon">📄</view>
<view class="file-name">{{proofurl.name}}</view>
<view class="file-delete" bindtap="deleteAnimalQuarantine">删除</view>
</view>
</view>
</view>
<!-- 贸易商特有字段 -->
<view class="section" wx:if="{{collaborationid === 'trader'}}">
<view class="section-title">法人身份证正反面(选填)</view>
<view class="upload-area" bindtap="uploadIdCard">
<view class="upload-icon">+</view>
<view class="upload-text">点击上传法人身份证正反面</view>
<view class="upload-tip">支持jpg、png格式,大小不超过5M</view>
</view>
<view wx:if="{{idCardFile}}">
<view class="uploaded-file">
<view class="file-icon">📄</view>
<view class="file-name">{{idCardFile.name}}</view>
<view class="file-delete" bindtap="deleteIdCard">删除</view>
</view>
</view>
</view>
<view class="section">
<view class="section-title">品牌授权链文件</view>
<view class="upload-area" bindtap="uploadBrandAuth">
<view class="upload-icon">+</view>
<view class="upload-text">点击上传品牌授权链文件</view>
<view class="upload-tip">支持jpg、png格式,大小不超过5M</view>
</view>
<view wx:if="{{brandurl}}">
<view class="uploaded-file">
<view class="file-icon">📄</view>
<view class="file-name">{{brandurl.name}}</view>
<view class="file-delete" bindtap="deleteBrandAuth">删除</view>
</view>
</view>
</view>
<button class="btn btn-primary" bindtap="submitApplication">提交申请</button>
<button class="btn btn-secondary" bindtap="prevStep">返回上一步</button>
</view>
<!-- 审核状态页面 -->
<view class="page" wx:if="{{currentStep === 3}}">
<!-- 审核中状态 -->
<view class="audit-status" wx:if="{{partnerstatus === 'underreview'}}">
<view class="audit-icon pending">
<text class="icon">⏳</text>
</view>
<view class="audit-title">审核中</view>
<view class="audit-desc">
你已成功提交小程序备案,请等待审核。<br />
你可以撤回备案
</view>
<button class="btn btn-outline" bindtap="withdrawApplication">撤回备案</button>
<button class="btn btn-primary btn-audit" bindtap="knowAudit">我知道了</button>
</view>
<!-- 审核失败状态 -->
<view class="audit-status" wx:if="{{partnerstatus === 'reviewfailed'}}">
<view class="audit-icon failed">
<text class="icon">❌</text>
</view>
<view class="audit-title">审核失败</view>
<view class="audit-desc">
很抱歉,您的备案申请未通过审核
</view>
<view class="audit-reason">
<view class="audit-reason-title">审核失败原因:</view>
<view class="audit-reason-content">{{auditFailedReason}}</view>
</view>
<button class="btn btn-primary btn-audit" bindtap="resubmitApplication">重新提交备案</button>
</view>
<!-- 审核通过状态 -->
<view class="audit-status" wx:if="{{partnerstatus === 'approved'}}">
<view class="audit-icon pending">
<text class="icon">✅</text>
</view>
<view class="audit-title">审核通过</view>
<view class="audit-desc">
恭喜!您的备案申请已通过审核。<br />
我们将尽快与您联系后续事宜。
</view>
<button class="btn btn-primary btn-audit" bindtap="completeApplication">完成</button>
</view>
<!-- 合作中状态 -->
<view class="audit-status" wx:if="{{partnerstatus === 'incooperation'}}">
<view class="audit-icon success">
<text class="icon">🤝</text>
</view>
<view class="audit-title">合作中</view>
<view class="audit-desc">
您已成功成为我们的合作伙伴!<br />
感谢您的信任与支持。
</view>
<button class="btn btn-primary btn-audit" bindtap="completeApplication">继续合作</button>
</view>
<!-- 未合作状态 -->
<view class="audit-status" wx:if="{{partnerstatus === 'notcooperative'}}">
<view class="audit-icon neutral">
<text class="icon">📋</text>
</view>
<view class="audit-title">未合作</view>
<view class="audit-desc">
感谢您的关注,期待未来有机会合作。<br />
如有需要可重新申请。
</view>
<button class="btn btn-outline" bindtap="resetApplication">重新申请</button>
<button class="btn btn-primary btn-audit" bindtap="knowAudit">我知道了</button>
</view>
</view>
</view>
</view>
</view>
<!-- 登录授权弹窗 -->
<view class="auth-modal" wx:if="{{showAuthModal}}">
<view class="auth-content">
<view class="auth-header">
<view class="auth-title">登录授权</view>
<view class="auth-close" bindtap="closeAuthModal">×</view>
</view>
<view class="auth-body">
<view class="auth-icon">📱</view>
<view class="auth-text">为了提供更好的服务,需要获取您的手机号进行身份验证</view>
<!-- 手机号授权按钮 -->
<button
class="auth-btn"
open-type="getPhoneNumber"
bindgetphonenumber="onGetPhoneNumber"
>
授权手机号
</button>
<view class="auth-tip">授权后即可完成入驻申请</view>
</view>
</view>
</view>

1697
pages/settlement/index.wxss

File diff suppressed because it is too large

66
pages/test-tools/api-test.js

@ -0,0 +1,66 @@
// pages/test-tools/api-test.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/test-tools/api-test.wxml

@ -0,0 +1,2 @@
<!--pages/test-tools/api-test.wxml-->
<text>pages/test-tools/api-test.wxml</text>

66
pages/test-tools/clear-storage.js

@ -0,0 +1,66 @@
// pages/test-tools/clear-storage.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/test-tools/clear-storage.wxml

@ -0,0 +1,2 @@
<!--pages/test-tools/clear-storage.wxml-->
<text>pages/test-tools/clear-storage.wxml</text>

66
pages/test-tools/connection-test.js

@ -0,0 +1,66 @@
// pages/test-tools/connection-test.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/test-tools/connection-test.wxml

@ -0,0 +1,2 @@
<!--pages/test-tools/connection-test.wxml-->
<text>pages/test-tools/connection-test.wxml</text>

66
pages/test-tools/fix-connection.js

@ -0,0 +1,66 @@
// pages/test-tools/fix-connection.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/test-tools/fix-connection.wxml

@ -0,0 +1,2 @@
<!--pages/test-tools/fix-connection.wxml-->
<text>pages/test-tools/fix-connection.wxml</text>

66
pages/test-tools/gross-weight-tester.js

@ -0,0 +1,66 @@
// pages/test-tools/gross-weight-tester.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/test-tools/gross-weight-tester.wxml

@ -0,0 +1,2 @@
<!--pages/test-tools/gross-weight-tester.wxml-->
<text>pages/test-tools/gross-weight-tester.wxml</text>

66
pages/test-tools/phone-test.js

@ -0,0 +1,66 @@
// pages/test-tools/phone-test.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/test-tools/phone-test.wxml

@ -0,0 +1,2 @@
<!--pages/test-tools/phone-test.wxml-->
<text>pages/test-tools/phone-test.wxml</text>

66
pages/test-tools/test-mode-switch.js

@ -0,0 +1,66 @@
// pages/test-tools/test-mode-switch.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/test-tools/test-mode-switch.wxml

@ -0,0 +1,2 @@
<!--pages/test-tools/test-mode-switch.wxml-->
<text>pages/test-tools/test-mode-switch.wxml</text>

66
pages/test/undercarriage-test.js

@ -0,0 +1,66 @@
// pages/test/undercarriage-test.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

2
pages/test/undercarriage-test.wxml

@ -0,0 +1,2 @@
<!--pages/test/undercarriage-test.wxml-->
<text>pages/test/undercarriage-test.wxml</text>

91
project.config.json

@ -0,0 +1,91 @@
{
"setting": {
"es6": true,
"postcss": true,
"minified": true,
"uglifyFileName": false,
"enhance": true,
"sourceMap": false,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true,
"requestDomain": [
"http://localhost:3000",
"http://8.137.125.67:3000",
"http://localhost:3001",
"https://youniao.icu"
],
"compileWorklet": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"localPlugins": false,
"disableUseStrict": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [
{
"value": "server-example",
"type": "folder"
},
{
"value": "node_modules",
"type": "folder"
},
{
"value": ".md",
"type": "suffix"
},
{
"value": "test-api-fix.js",
"type": "file"
},
{
"value": "test-minimal.js",
"type": "file"
},
{
"value": "test-ports.js",
"type": "file"
},
{
"value": "test-server.js",
"type": "file"
},
{
"value": "verify-fix.js",
"type": "file"
},
{
"value": "verify-login.js",
"type": "file"
},
{
"value": "check-files.js",
"type": "file"
},
{
"value": "full-debug.js",
"type": "file"
},
{
"value": "get-openid.js",
"type": "file"
}
],
"include": []
},
"appid": "wx3da6ea0adf91cf0d",
"editorSetting": {},
"libVersion": "3.10.3"
}

23
project.private.config.json

@ -0,0 +1,23 @@
{
"libVersion": "3.10.3",
"projectname": "miniprogram-x27",
"setting": {
"urlCheck": false,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true,
"useApiHook": true,
"useApiHostProcess": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false
},
"condition": {}
}

24
server-example/.env

@ -0,0 +1,24 @@
# 微信小程序配置
WECHAT_APPID=wx3da6ea0adf91cf0d
WECHAT_APPSECRET=78fd81bce5a2968a8e7c607ae68c4c0b
WECHAT_TOKEN=your-random-token
# MySQL数据库配置(请根据您的实际环境修改)
# 如果是首次使用,可能需要先在MySQL中创建wechat_app数据库
DB_HOST=1.95.162.61
DB_PORT=3306
DB_DATABASE=wechat_app
# 请使用您实际的MySQL用户名
DB_USER=root
# 请使用您实际的MySQL密码
# 如果MySQL的root用户有密码,请在此处填写
# 如果没有密码,请保留为空字符串(DB_PASSWORD="")
DB_PASSWORD=schl@2025
# 服务器配置
PORT=3003
# 日志配置
LOG_LEVEL=debug
NODE_ENV=development
# 详细日志记录,用于问题排查
ENABLE_DETAILED_LOGGING=true

101
server-example/.env.example.mysql

@ -0,0 +1,101 @@
# 微信小程序服务器环境变量配置示例(MySQL版本)
# 将此文件复制为 .env 文件并填写实际值
# ========================================================
# 微信小程序配置
# ========================================================
# 从微信公众平台获取的AppID
WECHAT_APPID=wx1234567890abcdef
# 从微信公众平台获取的AppSecret(请妥善保管,不要泄露)
WECHAT_APPSECRET=abcdef1234567890abcdef1234567890
# 微信消息校验Token(可选,用于消息验证)
WECHAT_TOKEN=your-random-token
# ========================================================
# MySQL数据库配置
# ========================================================
# MySQL主机地址(云数据库地址)
DB_HOST=your_mysql_host
# MySQL端口(默认3306)
DB_PORT=3306
# MySQL数据库名(主数据库)
DB_DATABASE=wechat_app
# MySQL用户名
DB_USER=your_mysql_username
# MySQL密码
DB_PASSWORD=your_mysql_password
# userlogin数据库名(如果需要连接)
DB_DATABASE_USERLOGIN=userlogin
# MySQL连接池配置
DB_MAX_CONNECTIONS=10
DB_MIN_CONNECTIONS=0
DB_CONNECTION_ACQUIRE_TIMEOUT=30000
DB_CONNECTION_IDLE_TIMEOUT=10000
# ========================================================
# 服务器配置
# ========================================================
# 服务器监听端口
PORT=3000
# 运行环境(development/production/test)
NODE_ENV=development
# 日志级别(debug/info/warn/error)
LOG_LEVEL=info
# 允许的跨域来源(多个来源用逗号分隔)
CORS_ORIGINS=https://your-miniprogram-domain.com,http://localhost:8080
# ========================================================
# 安全配置
# ========================================================
# JWT密钥(用于API认证,生成随机字符串)
JWT_SECRET=your-random-jwt-secret
# JWT过期时间(秒)
JWT_EXPIRES_IN=86400
# 加密密钥(用于敏感数据加密)
ENCRYPTION_KEY=your-encryption-key-32-bytes-length
# ========================================================
# 微信接口配置
# ========================================================
# 微信API基础URL
WECHAT_API_BASE_URL=https://api.weixin.qq.com
# 微信登录接口路径
WECHAT_LOGIN_PATH=/sns/jscode2session
# 微信接口请求超时时间(毫秒)
WECHAT_API_TIMEOUT=5000
# ========================================================
# 开发环境配置
# ========================================================
# 在开发环境中启用详细错误信息
DEVELOPMENT_SHOW_ERROR_DETAILS=true
# 是否启用请求日志记录
ENABLE_REQUEST_LOGGING=true
# ========================================================
# 生产环境配置
# ========================================================
# 是否启用Gzip压缩
PRODUCTION_ENABLE_GZIP=true
# 是否启用缓存
PRODUCTION_ENABLE_CACHE=true
# 缓存过期时间(秒)
PRODUCTION_CACHE_TTL=3600

41
server-example/add-department-column.js

@ -0,0 +1,41 @@
const { Sequelize } = require('sequelize');
require('dotenv').config();
// 创建数据库连接
const sequelize = new Sequelize(
process.env.DB_DATABASE || 'wechat_app',
process.env.DB_USER || 'root',
process.env.DB_PASSWORD === undefined ? null : process.env.DB_PASSWORD,
{
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
dialect: 'mysql',
timezone: '+08:00'
}
);
// 添加department字段到usermanagements表
async function addDepartmentColumn() {
try {
// 连接数据库
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 使用queryInterface添加字段
await sequelize.getQueryInterface().addColumn('usermanagements', 'department', {
type: Sequelize.STRING(255),
defaultValue: null,
comment: '部门信息'
});
console.log('✅ 成功添加department字段到usermanagements表');
} catch (error) {
console.error('❌ 添加字段失败:', error.message);
} finally {
// 关闭数据库连接
await sequelize.close();
}
}
// 执行函数
addDepartmentColumn();

143
server-example/complete-gross-weight-fix.js

@ -0,0 +1,143 @@
// 毛重字段(grossWeight)处理逻辑完整修复脚本
// 此脚本用于统一所有API接口对grossWeight字段的处理逻辑
const fs = require('fs');
const path = require('path');
// 定义配置
const config = {
serverFilePath: path.join(__dirname, 'server-mysql.js'),
backupFilePath: path.join(__dirname, 'server-mysql.js.bak.final-fix-' + Date.now()),
logFilePath: path.join(__dirname, 'final-fix-gross-weight-log.txt')
};
// 日志函数
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = '[' + timestamp + '] ' + message;
console.log(logMessage);
try {
fs.appendFileSync(config.logFilePath, logMessage + '\n');
} catch (e) {
console.error('写入日志文件失败:', e.message);
}
}
// 读取文件内容
function readFile(filePath) {
try {
return fs.readFileSync(filePath, 'utf8');
} catch (error) {
log('读取文件失败: ' + error.message);
throw error;
}
}
// 写入文件内容
function writeFile(filePath, content) {
try {
fs.writeFileSync(filePath, content, 'utf8');
log('文件已成功写入: ' + filePath);
} catch (error) {
log('写入文件失败: ' + error.message);
throw error;
}
}
// 创建备份文件
function createBackup() {
try {
const content = readFile(config.serverFilePath);
writeFile(config.backupFilePath, content);
log('已创建备份文件: ' + config.backupFilePath);
} catch (error) {
log('创建备份文件失败: ' + error.message);
throw error;
}
}
// 主函数
function main() {
log('===== 开始执行毛重字段处理逻辑完整修复 =====');
try {
// 创建备份
createBackup();
// 读取文件内容
let content = readFile(config.serverFilePath);
// 修复1: 统一中间件中的毛重处理逻辑,确保所有空值都设为5
const searchPatterns = [
'product.grossWeight = 0;',
'product.grossWeight = 0; // 空值设置为0'
];
let fixesApplied = 0;
searchPatterns.forEach(pattern => {
if (content.includes(pattern)) {
const originalCount = (content.match(new RegExp(pattern, 'g')) || []).length;
content = content.replace(new RegExp(pattern, 'g'), 'product.grossWeight = 5; // 空值设置为5');
const fixedCount = (content.match(/product\.grossWeight = 5;/g) || []).length - originalCount;
fixesApplied += fixedCount;
log('修复中间件中的毛重默认值: 替换了' + fixedCount + '处');
}
});
if (fixesApplied > 0) {
log('修复1完成: 已统一所有中间件中的毛重默认值为5');
} else {
log('修复1跳过: 所有中间件中的毛重默认值已经是5');
}
// 修复2: 在商品上传接口添加毛重处理逻辑
const uploadApiSearch = 'app.post(\'/api/products/upload\', async (req, res) => {';
if (content.includes(uploadApiSearch)) {
// 查找上传接口的位置
const uploadApiStart = content.indexOf(uploadApiSearch);
const uploadApiEnd = content.indexOf('});', uploadApiStart) + 3;
const uploadApiContent = content.substring(uploadApiStart, uploadApiEnd);
// 检查是否已经包含毛重处理逻辑
if (uploadApiContent.includes('grossWeight') && uploadApiContent.includes('parseFloat')) {
log('修复2跳过: 商品上传接口已经包含毛重处理逻辑');
} else {
// 查找商品数据处理的位置(在try块内)
const tryBlockStart = uploadApiContent.indexOf('try {');
const tryBlockEnd = uploadApiContent.lastIndexOf('} catch');
if (tryBlockStart !== -1 && tryBlockEnd !== -1) {
// 在try块开始处添加毛重处理逻辑
const tryBlockContent = uploadApiContent.substring(tryBlockStart, tryBlockEnd);
const weightHandlingCode = `try {\n // 修复毛重字段处理逻辑\n if (req.body && req.body.productData) {\n let processedGrossWeight = 5; // 默认值为5\n if (req.body.productData.grossWeight !== null && req.body.productData.grossWeight !== undefined && req.body.productData.grossWeight !== \'\') {\n const numValue = parseFloat(req.body.productData.grossWeight);\n if (!isNaN(numValue) && isFinite(numValue)) {\n processedGrossWeight = numValue;\n }\n }\n req.body.productData.grossWeight = processedGrossWeight;\n console.log(\'修复后 - 毛重值处理: 原始值=\' + (req.body.productData.grossWeight || \'undefined\') + ', 处理后=\' + processedGrossWeight);\n }`;
// 替换原代码
const fixedUploadApiContent = uploadApiContent.replace(tryBlockContent, weightHandlingCode);
content = content.replace(uploadApiContent, fixedUploadApiContent);
log('修复2完成: 在商品上传接口添加了毛重处理逻辑');
} else {
log('修复2失败: 无法在商品上传接口中找到try-catch块');
}
}
} else {
log('修复2跳过: 未找到商品上传接口');
}
// 写入修复后的内容
writeFile(config.serverFilePath, content);
log('===== 毛重字段处理逻辑完整修复完成 =====');
log('修复内容总结:');
log('1. 统一了所有中间件中的毛重默认值为5');
log('2. 在商品上传接口中添加了毛重处理逻辑,将空值设为5,有效数字转换为float类型');
log('3. 创建了备份文件,以便需要时恢复');
} catch (error) {
log('修复过程中发生错误: ' + error.message);
log('===== 毛重字段处理逻辑完整修复失败 =====');
process.exit(1);
}
}
// 执行主函数
main();

123
server-example/complete-gross-weight-verification.js

@ -0,0 +1,123 @@
const fs = require('fs');
const path = require('path');
// 读取server-mysql.js文件内容
function readServerFile() {
return fs.readFileSync(path.join(__dirname, 'server-mysql.js'), 'utf8');
}
// 验证毛重字段处理逻辑
function verifyGrossWeightHandling() {
try {
const fileContent = readServerFile();
// 初始化验证结果
const verificationResult = {
totalIssues: 0,
successPoints: 0,
issues: [],
successDetails: []
};
// 检查响应中间件中的/products/list接口
const listMiddlewarePattern = /data\.products\s*=\s*data\.products\.map\(product\s*=>\s*\{[\s\S]*?product\.grossWeight\s*=\s*([^;]+);/;
const listMiddlewareMatch = fileContent.match(listMiddlewarePattern);
if (listMiddlewareMatch && listMiddlewareMatch[1].includes('0')) {
verificationResult.successPoints++;
verificationResult.successDetails.push('✓ 响应中间件(/products/list)已正确设置空毛重默认值为0');
} else {
verificationResult.totalIssues++;
verificationResult.issues.push('✗ 响应中间件(/products/list)未正确设置空毛重默认值');
}
// 检查响应中间件中的/data接口
const dataMiddlewarePattern = /data\.data\.products\s*=\s*data\.data\.products\.map\(product\s*=>\s*\{[\s\S]*?product\.grossWeight\s*=\s*([^;]+);/;
const dataMiddlewareMatch = fileContent.match(dataMiddlewarePattern);
if (dataMiddlewareMatch && dataMiddlewareMatch[1].includes('0')) {
verificationResult.successPoints++;
verificationResult.successDetails.push('✓ 响应中间件(/data)已正确设置空毛重默认值为0');
} else {
verificationResult.totalIssues++;
verificationResult.issues.push('✗ 响应中间件(/data)未正确设置空毛重默认值');
}
// 检查商品上传接口
const uploadApiPattern = /app\.post\('\/api\/products\/upload',[\s\S]*?let\s+processedGrossWeight\s*=\s*(\d+)/;
const uploadApiMatch = fileContent.match(uploadApiPattern);
if (uploadApiMatch && uploadApiMatch[1] === '0') {
verificationResult.successPoints++;
verificationResult.successDetails.push('✓ 商品上传接口已正确设置空毛重默认值为0');
} else {
verificationResult.totalIssues++;
verificationResult.issues.push('✗ 商品上传接口未正确设置空毛重默认值');
}
// 检查编辑商品API
const editApiPattern = /parsedValue:\s*product\.grossWeight\s*===\s*''\s*\|\|\s*product\.grossWeight\s*===\s*null\s*\|\|\s*product\.grossWeight\s*===\s*undefined\s*\?\s*(\d+)/;
const editApiMatch = fileContent.match(editApiPattern);
if (editApiMatch && editApiMatch[1] === '0') {
verificationResult.successPoints++;
verificationResult.successDetails.push('✓ 编辑商品API已正确设置空毛重默认值为0');
} else {
verificationResult.totalIssues++;
verificationResult.issues.push('✗ 编辑商品API未正确设置空毛重默认值');
}
// 检查是否还有设置为5的地方
const remaining5Pattern = /grossWeight\s*=\s*5/g;
const remaining5Matches = fileContent.match(remaining5Pattern);
if (remaining5Matches && remaining5Matches.length > 0) {
verificationResult.totalIssues += remaining5Matches.length;
verificationResult.issues.push(`✗ 发现${remaining5Matches.length}处仍将毛重设置为5的地方`);
} else {
verificationResult.successPoints++;
verificationResult.successDetails.push('✓ 未发现仍将毛重设置为5的残留代码');
}
// 检查是否正确实现了空值返回0的逻辑
const emptyValueHandlingPattern = /product\.grossWeight\s*===\s*null\s*\|\|\s*product\.grossWeight\s*===\s*undefined\s*\|\|\s*product\.grossWeight\s*===\s*''\s*\?\s*0/g;
const emptyValueMatches = fileContent.match(emptyValueHandlingPattern);
if (emptyValueMatches && emptyValueMatches.length > 0) {
verificationResult.successPoints++;
verificationResult.successDetails.push(`✓ 发现${emptyValueMatches.length}处正确实现了空值返回0的逻辑`);
}
// 输出验证结果
console.log('\n======== 毛重字段处理逻辑全面验证结果 ========');
console.log('\n成功项:');
verificationResult.successDetails.forEach(detail => console.log(detail));
console.log('\n问题项:');
if (verificationResult.issues.length === 0) {
console.log('✓ 未发现任何问题');
} else {
verificationResult.issues.forEach(issue => console.log(issue));
}
console.log('\n总体评估:');
if (verificationResult.totalIssues === 0) {
console.log('✅ 验证成功: 所有毛重字段处理逻辑已正确实现');
console.log(' 已满足要求: 空值时小程序和数据库均返回0,非空值返回实际值');
} else {
console.log(`❌ 验证失败: 发现${verificationResult.totalIssues}个问题需要修复`);
}
console.log('==============================================');
// 设置退出码
process.exit(verificationResult.totalIssues > 0 ? 1 : 0);
} catch (error) {
console.error('验证过程中发生错误:', error);
process.exit(1);
}
}
// 执行验证
verifyGrossWeightHandling();

155
server-example/create-missing-associations.js

@ -0,0 +1,155 @@
const { Sequelize } = require('sequelize');
const fs = require('fs');
const path = require('path');
// 读取环境变量
const envPath = path.join(__dirname, '.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf8');
const envVars = envContent.split('\n').filter(line => line.trim() && !line.startsWith('#'));
envVars.forEach(line => {
const [key, value] = line.split('=').map(part => part.trim());
process.env[key] = value;
});
}
// 数据库连接配置
const sequelize = new Sequelize(
process.env.DB_NAME || 'wechat_app',
process.env.DB_USER || 'root',
process.env.DB_PASSWORD || '',
{
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
dialect: 'mysql',
logging: false,
timezone: '+08:00' // 设置时区为UTC+8
}
);
// 定义模型 - 简化版
const User = sequelize.define('User', {
userId: {
type: sequelize.Sequelize.STRING(100),
primaryKey: true,
allowNull: false
},
nickName: sequelize.Sequelize.STRING(100),
phoneNumber: sequelize.Sequelize.STRING(20)
}, {
tableName: 'users',
timestamps: false
});
const Contact = sequelize.define('Contact', {
id: {
type: sequelize.Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: sequelize.Sequelize.STRING(100),
allowNull: false
},
nickName: sequelize.Sequelize.STRING(100),
phoneNumber: sequelize.Sequelize.STRING(20)
}, {
tableName: 'contacts',
timestamps: false
});
const UserManagement = sequelize.define('UserManagement', {
id: {
type: sequelize.Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: sequelize.Sequelize.STRING(100),
allowNull: false
}
}, {
tableName: 'usermanagements',
timestamps: false
});
// 修复函数
async function fixMissingAssociations() {
try {
console.log('========================================');
console.log('开始修复用户关联表记录');
console.log('========================================');
// 连接数据库
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 获取所有用户
const users = await User.findAll();
console.log(`📊 共找到 ${users.length} 个用户记录`);
let contactsCreated = 0;
let managementsCreated = 0;
// 为每个用户检查并创建关联记录
for (let i = 0; i < users.length; i++) {
const user = users[i];
console.log(`\n🔄 处理用户 ${i + 1}/${users.length}: ${user.userId}`);
// 检查并创建联系人记录
try {
const existingContact = await Contact.findOne({
where: { userId: user.userId }
});
if (!existingContact) {
await Contact.create({
userId: user.userId,
nickName: user.nickName || '默认联系人',
phoneNumber: user.phoneNumber || ''
});
console.log('✅ 创建了联系人记录');
contactsCreated++;
} else {
console.log('✅ 联系人记录已存在');
}
} catch (error) {
console.error('❌ 创建联系人记录失败:', error.message);
}
// 检查并创建用户管理记录
try {
const existingManagement = await UserManagement.findOne({
where: { userId: user.userId }
});
if (!existingManagement) {
await UserManagement.create({
userId: user.userId
});
console.log('✅ 创建了用户管理记录');
managementsCreated++;
} else {
console.log('✅ 用户管理记录已存在');
}
} catch (error) {
console.error('❌ 创建用户管理记录失败:', error.message);
}
}
console.log('\n========================================');
console.log('修复完成!');
console.log(`📈 共创建了 ${contactsCreated} 条联系人记录`);
console.log(`📈 共创建了 ${managementsCreated} 条用户管理记录`);
console.log('========================================');
} catch (error) {
console.error('❌ 修复过程中发生错误:', error);
} finally {
// 关闭数据库连接
await sequelize.close();
}
}
// 运行修复
fixMissingAssociations();

356
server-example/database-extension.js

@ -0,0 +1,356 @@
// 注意:此文件是MongoDB版本的扩展实现,已被禁用
// 数据库扩展 - 用于连接userlogin数据库并关联表
const { Sequelize, DataTypes, Model } = require('sequelize');
const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '.env') });
// 注意:不再直接导入User模型以避免循环依赖
// User模型将通过setupAssociations函数的参数传入
let User = null;
// 创建到userlogin数据库的连接
const sequelizeUserLogin = new Sequelize(
process.env.DB_DATABASE_USERLOGIN || 'userlogin',
process.env.DB_USER || 'root',
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
dialect: 'mysql',
pool: {
max: 10,
min: 0,
acquire: 30000,
idle: 10000
},
timezone: '+08:00' // 设置时区为UTC+8
}
);
// 测试userlogin数据库连接
async function testUserLoginDbConnection() {
try {
await sequelizeUserLogin.authenticate();
console.log('userlogin数据库连接成功');
} catch (error) {
console.error('userlogin数据库连接失败:', error);
console.error('请注意:如果不需要使用userlogin数据库,可以忽略此错误');
}
}
// 定义userlogin数据库中的表模型
// contact表模型
class Contact extends Model { }
Contact.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: DataTypes.STRING(100),
allowNull: false
},
name: {
type: DataTypes.STRING(100),
allowNull: false
},
phone: {
type: DataTypes.STRING(20),
allowNull: false
},
email: {
type: DataTypes.STRING(100)
},
address: {
type: DataTypes.TEXT
},
created_at: {
type: DataTypes.DATE,
defaultValue: Sequelize.NOW
}
}, {
sequelize: sequelizeUserLogin,
modelName: 'Contact',
tableName: 'contact',
timestamps: false
});
// enterprise表模型
class Enterprise extends Model { }
Enterprise.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: DataTypes.STRING(100),
allowNull: false
},
enterpriseName: {
type: DataTypes.STRING(255),
allowNull: false
},
businessLicense: {
type: DataTypes.STRING(255)
},
address: {
type: DataTypes.TEXT
},
contactPerson: {
type: DataTypes.STRING(100)
},
contactPhone: {
type: DataTypes.STRING(20)
},
created_at: {
type: DataTypes.DATE,
defaultValue: Sequelize.NOW
}
}, {
sequelize: sequelizeUserLogin,
modelName: 'Enterprise',
tableName: 'enterprise',
timestamps: false
});
// managers表模型
class Manager extends Model { }
Manager.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: DataTypes.STRING(100),
allowNull: false
},
managerName: {
type: DataTypes.STRING(100),
allowNull: false
},
managerPhone: {
type: DataTypes.STRING(20),
allowNull: false
},
role: {
type: DataTypes.STRING(50),
allowNull: false
},
created_at: {
type: DataTypes.DATE,
defaultValue: Sequelize.NOW
}
}, {
sequelize: sequelizeUserLogin,
modelName: 'Manager',
tableName: 'managers',
timestamps: false
});
// publicseademand表模型
class PublicSeaDemand extends Model { }
PublicSeaDemand.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: DataTypes.STRING(100),
allowNull: false
},
demandType: {
type: DataTypes.STRING(100),
allowNull: false
},
description: {
type: DataTypes.TEXT
},
status: {
type: DataTypes.STRING(50),
defaultValue: 'pending'
},
created_at: {
type: DataTypes.DATE,
defaultValue: Sequelize.NOW
}
}, {
sequelize: sequelizeUserLogin,
modelName: 'PublicSeaDemand',
tableName: 'publicseademand',
timestamps: false
});
// rootdb表模型
class RootDb extends Model { }
RootDb.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: DataTypes.STRING(100),
allowNull: false
},
dataKey: {
type: DataTypes.STRING(100),
allowNull: false
},
dataValue: {
type: DataTypes.TEXT
},
created_at: {
type: DataTypes.DATE,
defaultValue: Sequelize.NOW
}
}, {
sequelize: sequelizeUserLogin,
modelName: 'RootDb',
tableName: 'rootdb',
timestamps: false
});
// login表模型
class Login extends Model { }
Login.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: DataTypes.STRING(100),
allowNull: false
},
loginTime: {
type: DataTypes.DATE,
defaultValue: Sequelize.NOW
},
loginIp: {
type: DataTypes.STRING(50)
},
deviceInfo: {
type: DataTypes.TEXT
},
status: {
type: DataTypes.STRING(20),
defaultValue: 'success'
}
}, {
sequelize: sequelizeUserLogin,
modelName: 'Login',
tableName: 'login',
timestamps: false
});
// 设置模型关联关系
function setupAssociations(mainUserModel) {
// 确保使用传入的User模型,不再依赖默认的User变量
if (!mainUserModel) {
console.error('User模型未提供,无法设置关联关系');
return;
}
try {
// 关联User与Contact(一对多)
// 使用唯一的别名userContacts以避免可能的冲突
mainUserModel.hasMany(Contact, {
foreignKey: 'userId',
sourceKey: 'userId',
as: 'userContacts'
});
// 反向关联Contact与User
Contact.belongsTo(mainUserModel, {
foreignKey: 'userId',
targetKey: 'userId',
as: 'user'
});
// 关联User与Enterprise(一对多)
mainUserModel.hasMany(Enterprise, {
foreignKey: 'userId',
sourceKey: 'userId',
as: 'userEnterprises'
});
// 反向关联Enterprise与User
Enterprise.belongsTo(mainUserModel, {
foreignKey: 'userId',
targetKey: 'userId',
as: 'user'
});
// 关联User与Manager(一对多)
mainUserModel.hasMany(Manager, {
foreignKey: 'userId',
sourceKey: 'userId',
as: 'userManagers'
});
// 反向关联Manager与User
Manager.belongsTo(mainUserModel, {
foreignKey: 'userId',
targetKey: 'userId',
as: 'user'
});
// 关联User与PublicSeaDemand(一对多)
mainUserModel.hasMany(PublicSeaDemand, {
foreignKey: 'userId',
sourceKey: 'userId',
as: 'userPublicSeaDemands'
});
// 反向关联PublicSeaDemand与User
PublicSeaDemand.belongsTo(mainUserModel, {
foreignKey: 'userId',
targetKey: 'userId',
as: 'user'
});
// 关联User与RootDb(一对多)
mainUserModel.hasMany(RootDb, {
foreignKey: 'userId',
sourceKey: 'userId',
as: 'userRootDbs'
});
// 反向关联RootDb与User
RootDb.belongsTo(mainUserModel, {
foreignKey: 'userId',
targetKey: 'userId',
as: 'user'
});
// 关联User与Login(一对多)
mainUserModel.hasMany(Login, {
foreignKey: 'userId',
sourceKey: 'userId',
as: 'userLoginRecords'
});
console.log('已设置wechat_app数据库的User模型与userlogin数据库表的关联关系');
} catch (error) {
console.error('设置模型关联关系时出错:', error);
}
}
// 导出所有模型和连接
module.exports = {
sequelizeUserLogin,
testUserLoginDbConnection,
Contact,
Enterprise,
Manager,
PublicSeaDemand,
RootDb,
Login,
setupAssociations,
User // 导出User模型(可能是实际的模型或临时模型)
};

175
server-example/direct-db-check.js

@ -0,0 +1,175 @@
// 直接连接数据库检查productQuantity字段的脚本
const Sequelize = require('sequelize');
const mysql = require('mysql2/promise');
// 数据库连接配置
const sequelize = new Sequelize(
'minishop', // 数据库名
'root', // 用户名
'password', // 密码
{
host: 'localhost',
dialect: 'mysql',
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
timezone: '+08:00' // 设置时区为UTC+8
}
);
// 定义购物车模型 - 直接复制自server-mysql.js
class CartItem extends Sequelize.Model {}
CartItem.init({
id: {
type: Sequelize.DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: Sequelize.DataTypes.STRING(100),
allowNull: false
},
productId: {
type: Sequelize.DataTypes.STRING(100),
allowNull: false
},
productName: {
type: Sequelize.DataTypes.STRING(255),
allowNull: false
},
specification: {
type: Sequelize.DataTypes.STRING(255)
},
quantity: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false,
defaultValue: 1
},
productQuantity: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
grossWeight: {
type: Sequelize.DataTypes.DECIMAL(10, 2)
},
yolk: {
type: Sequelize.DataTypes.STRING(100)
},
price: {
type: Sequelize.DataTypes.DECIMAL(10, 2)
},
selected: {
type: Sequelize.DataTypes.BOOLEAN,
defaultValue: true
},
added_at: {
type: Sequelize.DataTypes.DATE,
defaultValue: Sequelize.NOW
}
}, {
sequelize,
modelName: 'CartItem',
tableName: 'cart_items',
timestamps: false
});
// 检查数据库结构
async function checkDatabaseStructure() {
console.log('开始直接检查数据库中的productQuantity字段...');
try {
// 检查连接
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 1. 使用原始查询检查表结构
console.log('\n1. 检查cart_items表结构...');
const [fields, _] = await sequelize.query('DESCRIBE cart_items');
// 查找productQuantity字段
const productQuantityField = fields.find(field => field.Field === 'productQuantity');
if (productQuantityField) {
console.log('✅ 数据库中存在productQuantity字段:');
console.log(` - 类型: ${productQuantityField.Type}`);
console.log(` - 是否允许NULL: ${productQuantityField.Null === 'YES' ? '是' : '否'}`);
console.log(` - 默认值: ${productQuantityField.Default || '无'}`);
} else {
console.error('❌ 数据库中不存在productQuantity字段!');
console.log('cart_items表中的所有字段:', fields.map(field => field.Field).join(', '));
// 如果不存在,尝试添加这个字段
console.log('\n尝试添加productQuantity字段到cart_items表...');
try {
await sequelize.query('ALTER TABLE cart_items ADD COLUMN productQuantity INT NOT NULL DEFAULT 0');
console.log('✅ 成功添加productQuantity字段');
} catch (addError) {
console.error('❌ 添加字段失败:', addError.message);
}
}
// 2. 检查test_user_id的购物车数据
console.log('\n2. 检查测试用户的购物车数据...');
const cartItems = await CartItem.findAll({
where: {
userId: 'test_user_id'
},
// 明确指定返回所有字段
attributes: {
exclude: [] // 不排除任何字段
}
});
console.log(`找到 ${cartItems.length} 条购物车记录`);
if (cartItems.length > 0) {
// 显示第一条记录的所有字段
console.log('\n第一条购物车记录的所有字段:');
const firstItem = cartItems[0].toJSON();
Object.keys(firstItem).forEach(key => {
console.log(` - ${key}: ${firstItem[key]}`);
});
// 特别检查productQuantity字段
console.log('\nproductQuantity字段在数据中的状态:');
cartItems.forEach((item, index) => {
const data = item.toJSON();
console.log(` 记录 ${index + 1}: productQuantity = ${data.productQuantity !== undefined ? data.productQuantity : 'undefined'}`);
});
}
// 3. 尝试直接插入一条带productQuantity的记录
console.log('\n3. 尝试直接插入一条带productQuantity的记录...');
const testProductId = 'db_test_' + Date.now();
const newItem = await CartItem.create({
userId: 'test_user_id',
productId: testProductId,
productName: '数据库测试商品',
specification: '测试规格',
quantity: 2,
productQuantity: 10,
grossWeight: 1000,
yolk: '测试蛋黄',
price: 50,
selected: true,
added_at: new Date()
});
console.log('✅ 成功插入记录');
console.log('插入的记录详情:', newItem.toJSON());
} catch (error) {
console.error('检查过程中发生错误:', error.message);
} finally {
// 关闭连接
await sequelize.close();
console.log('\n数据库连接已关闭');
}
}
// 执行检查
checkDatabaseStructure();

86
server-example/ecosystem.config.js

@ -0,0 +1,86 @@
module.exports = {
apps: [
{
// 应用名称
name: 'wechat-app',
// 要运行的脚本 - 修正为正确的启动脚本
script: 'start-server.js',
// 运行模式 - 设置为fork模式以避免端口冲突
exec_mode: 'fork',
// 实例数量
instances: 1,
// 自动重启
autorestart: true,
// 监听文件变化(开发环境可设置为true)
watch: false,
// 内存限制,超过则重启
max_memory_restart: '1G',
// 环境变量
env: {
NODE_ENV: 'production',
PORT: 3002
},
env_development: {
NODE_ENV: 'development',
PORT: 3002
},
// 日志配置 - 重要:这些配置确保PM2正确捕获和显示所有console.log输出
log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS',
error_file: './logs/error.log',
out_file: './logs/output.log',
merge_logs: true,
combine_logs: false, // 确保不合并日志
log_file_max_size: '10MB',
// 启用日志时间戳
time: true,
// 自动重启策略
min_uptime: '60s',
max_restarts: 10,
restart_delay: 1000,
// 环境变量文件加载
// PM2会自动加载应用目录下的.env文件
// 附加参数
args: [],
// 启动超时时间
kill_timeout: 5000,
// 启动前的钩子
pre_start: "echo 'Starting wechat-miniprogram-server...'",
// 启动后的钩子
post_start: "echo 'wechat-miniprogram-server started successfully'",
// 错误处理
error: './logs/process-error.log',
// 合并stdout和stderr
combine_logs: false
}
],
// 简化部署配置,用户可以根据实际情况修改
deploy: {
production: {
user: 'your-username',
host: 'your-server-ip',
ref: 'origin/master',
repo: 'your-git-repository-url',
path: '/path/to/your/app',
'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production'
}
}
};
/*
使用说明
1. 基本启动pm2 start ecosystem.config.js
2. 开发环境启动pm2 start ecosystem.config.js --env development
3. 查看状态pm2 status
4. 查看日志pm2 logs wechat-miniprogram-server
5. 监控应用pm2 monit
6. 停止应用pm2 stop wechat-miniprogram-server
7. 重启应用pm2 restart wechat-miniprogram-server
8. 删除应用pm2 delete wechat-miniprogram-server
9. 设置开机自启pm2 startup && pm2 save
故障排除提示
- 如果应用启动失败查看日志pm2 logs wechat-miniprogram-server --lines 100
- 检查端口占用lsof -i :3001 netstat -ano | findstr :3001
- 确保.env文件配置正确
- 确保数据库服务正常运行
*/

61
server-example/find-product-creator.js

@ -0,0 +1,61 @@
// 查询特定名称商品的创建者
const dotenv = require('dotenv');
const mysql = require('mysql2/promise');
const path = require('path');
// 加载环境变量
dotenv.config({ path: path.resolve(__dirname, '.env') });
// 数据库连接配置
const dbConfig = {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'wechat_app',
timezone: '+08:00' // 设置时区为UTC+8
};
async function findProductCreator() {
let connection;
try {
// 连接数据库
connection = await mysql.createConnection(dbConfig);
console.log('数据库连接成功');
// 查询名称为88888的商品及其创建者
const [products] = await connection.query(`
SELECT p.productId, p.productName, p.sellerId, u.userId, u.nickName, u.phoneNumber
FROM products p
LEFT JOIN users u ON p.sellerId = u.userId
WHERE p.productName = '88888'
`);
if (products.length === 0) {
console.log('未找到名称为88888的商品');
return;
}
console.log(`找到 ${products.length} 个名称为88888的商品:`);
products.forEach((product, index) => {
console.log(`\n商品 ${index + 1}:`);
console.log(` 商品ID: ${product.productId}`);
console.log(` 商品名称: ${product.productName}`);
console.log(` 创建者ID: ${product.sellerId}`);
console.log(` 创建者昵称: ${product.nickName || '未设置'}`);
console.log(` 创建者手机号: ${product.phoneNumber || '未设置'}`);
});
} catch (error) {
console.error('查询失败:', error.message);
} finally {
if (connection) {
await connection.end();
console.log('\n数据库连接已关闭');
}
}
}
// 执行查询
findProductCreator();

85
server-example/fixed-server.js

@ -0,0 +1,85 @@
// 简单测试服务器 - 不连接数据库,专注于API接口测试和毛重字段处理
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
// 创建Express应用
const app = express();
const PORT = 3000;
// 中间件
app.use(bodyParser.json());
// 请求日志中间件
app.use((req, res, next) => {
const now = new Date();
console.log(`[${now.toISOString()}] 收到请求: ${req.method} ${req.url}`);
next();
});
// 简单测试接口
app.get('/api/test-connection', (req, res) => {
res.json({
success: true,
message: '服务器连接测试成功',
timestamp: new Date().toISOString(),
serverInfo: { port: PORT }
});
});
// 商品发布接口(简化版,专注于毛重处理)
app.post('/api/product/publish', (req, res) => {
try {
const { openid, product } = req.body;
console.log('收到商品发布请求:', { openid, product });
// 验证参数
if (!openid || !product) {
return res.status(400).json({ success: false, message: '缺少必要参数' });
}
// 重点:毛重字段处理逻辑
let grossWeightValue = product.grossWeight;
console.log('原始毛重值:', grossWeightValue, '类型:', typeof grossWeightValue);
// 处理各种情况的毛重值
if (grossWeightValue === '' || grossWeightValue === null || grossWeightValue === undefined || (typeof grossWeightValue === 'object' && grossWeightValue === null)) {
grossWeightValue = null;
console.log('毛重值为空或null,设置为null');
} else {
// 转换为数字
const numValue = Number(grossWeightValue);
if (!isNaN(numValue) && isFinite(numValue)) {
grossWeightValue = numValue;
console.log('毛重值成功转换为数字:', grossWeightValue);
} else {
grossWeightValue = null;
console.log('毛重值不是有效数字,设置为null');
}
}
// 返回处理结果
return res.json({
success: true,
message: '商品发布处理成功(模拟)',
processedData: {
productName: product.productName,
price: product.price,
quantity: product.quantity,
grossWeight: grossWeightValue, // 返回处理后的毛重值
grossWeightType: typeof grossWeightValue
}
});
} catch (error) {
console.error('发布商品失败:', error);
res.status(500).json({ success: false, message: '服务器错误', error: error.message });
}
});
// 启动服务器
app.listen(PORT, () => {
console.log(`修复版服务器运行在 http://localhost:${PORT}`);
console.log('测试接口: http://localhost:3000/api/test-connection');
console.log('商品发布接口: POST http://localhost:3000/api/product/publish');
});

87
server-example/free-port.js

@ -0,0 +1,87 @@
// 检查并释放被占用的端口
const { exec } = require('child_process');
const os = require('os');
// 要检查的端口
const PORT = 3001;
function killProcessOnPort(port) {
console.log(`开始检查端口 ${port} 的占用情况...`);
if (os.platform() === 'win32') {
// Windows系统
exec(`netstat -ano | findstr :${port}`, (error, stdout) => {
if (error) {
console.log(`端口 ${port} 未被占用`);
return;
}
const lines = stdout.trim().split('\n');
if (lines.length > 0) {
// 提取PID
const pid = lines[0].trim().split(/\s+/).pop();
console.log(`发现进程 ${pid} 占用端口 ${port}`);
// 杀死进程
exec(`taskkill /F /PID ${pid}`, (killError) => {
if (killError) {
console.error(`杀死进程 ${pid} 失败:`, killError.message);
} else {
console.log(`成功杀死进程 ${pid},端口 ${port} 已释放`);
}
});
} else {
console.log(`端口 ${port} 未被占用`);
}
});
} else {
// Linux/Mac系统
exec(`lsof -i :${port}`, (error, stdout) => {
if (error) {
console.log(`端口 ${port} 未被占用`);
return;
}
const lines = stdout.trim().split('\n');
if (lines.length > 1) {
// 提取PID
const pid = lines[1].trim().split(/\s+/)[1];
console.log(`发现进程 ${pid} 占用端口 ${port}`);
// 杀死进程
exec(`kill -9 ${pid}`, (killError) => {
if (killError) {
console.error(`杀死进程 ${pid} 失败:`, killError.message);
} else {
console.log(`成功杀死进程 ${pid},端口 ${port} 已释放`);
}
});
} else {
console.log(`端口 ${port} 未被占用`);
}
});
}
}
// 执行端口检查和释放
killProcessOnPort(PORT);
// 延迟2秒后再次启动服务器(如果是Windows系统)
setTimeout(() => {
if (os.platform() === 'win32') {
console.log('\n正在尝试重新启动服务器...');
const serverProcess = exec('node server-mysql.js');
serverProcess.stdout.on('data', (data) => {
console.log(`服务器输出: ${data}`);
});
serverProcess.stderr.on('data', (data) => {
console.error(`服务器错误: ${data}`);
});
serverProcess.on('close', (code) => {
console.log(`服务器进程退出,代码: ${code}`);
});
}
}, 2000);

5
server-example/gross-weight-fix-error.json

@ -0,0 +1,5 @@
{
"timestamp": "2025-10-08T03:56:27.607Z",
"error": "Access denied for user 'root'@'218.88.54.38' (using password: YES)",
"stack": "Error: Access denied for user 'root'@'218.88.54.38' (using password: YES)\n at Object.createConnectionPromise [as createConnection] (D:\\WeichatAPP\\miniprogram-6\\server-example\\node_modules\\mysql2\\promise.js:19:31)\n at fixGrossWeightValues (D:\\WeichatAPP\\miniprogram-6\\server-example\\fix-gross-weight-values.js:26:30)\n at Object.<anonymous> (D:\\WeichatAPP\\miniprogram-6\\server-example\\fix-gross-weight-values.js:143:1)\n at Module._compile (node:internal/modules/cjs/loader:1688:14)\n at Object..js (node:internal/modules/cjs/loader:1820:10)\n at Module.load (node:internal/modules/cjs/loader:1423:32)\n at Function._load (node:internal/modules/cjs/loader:1246:12)\n at TracingChannel.traceSync (node:diagnostics_channel:322:14)\n at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)"
}

24
server-example/gross-weight-frontend-fix-report.json

@ -0,0 +1,24 @@
{
"timestamp": "2025-10-08T03:57:52.452Z",
"modified": true,
"changes": [
{
"name": "商品列表API增强",
"applied": true
},
{
"name": "用户商品API增强",
"applied": false
},
{
"name": "添加毛重处理中间件",
"applied": true
}
],
"recommendations": [
"重启服务器",
"检查前端页面使用的字段名",
"添加商品发布表单的毛重验证",
"检查前端数据处理逻辑"
]
}

135
server-example/gross-weight-log-analyzer.js

@ -0,0 +1,135 @@
const fs = require('fs');
const path = require('path');
// 日志文件路径
const logFilePath = path.join(__dirname, 'logs', 'output.log');
// 读取并分析日志文件
function analyzeGrossWeightLogs() {
try {
console.log(`正在分析日志文件: ${logFilePath}`);
console.log('搜索与grossWeight相关的日志记录...\n');
// 读取日志文件内容
const logContent = fs.readFileSync(logFilePath, 'utf-8');
const logLines = logContent.split('\n');
// 存储找到的毛重相关记录
const grossWeightRecords = [];
const publishRequestRecords = [];
// 搜索最近24小时的日志记录
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
// 遍历日志行
for (let i = 0; i < logLines.length; i++) {
const line = logLines[i];
// 检查时间戳是否在最近24小时内
const timestampMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})/);
if (timestampMatch) {
const logDate = new Date(timestampMatch[1]);
if (logDate < twentyFourHoursAgo) {
continue; // 跳过24小时前的日志
}
}
// 搜索与grossWeight相关的记录
if (line.includes('grossWeight')) {
grossWeightRecords.push({
line: i + 1,
content: line,
timestamp: timestampMatch ? timestampMatch[1] : '未知'
});
}
// 搜索商品发布请求
if (line.includes('/api/product/publish')) {
// 收集发布请求的上下文
const contextLines = [];
// 向前收集5行
for (let j = Math.max(0, i - 5); j < Math.min(logLines.length, i + 20); j++) {
contextLines.push(logLines[j]);
}
publishRequestRecords.push({
line: i + 1,
content: contextLines.join('\n'),
timestamp: timestampMatch ? timestampMatch[1] : '未知'
});
}
}
// 输出分析结果
console.log('===== 最近24小时毛重字段处理分析结果 =====\n');
console.log(`找到 ${grossWeightRecords.length} 条与grossWeight相关的日志记录\n`);
// 显示最近的10条毛重记录
console.log('最近的10条毛重处理记录:');
grossWeightRecords.slice(-10).forEach((record, index) => {
console.log(`[${record.timestamp}] 第${record.line}行: ${record.content}`);
});
console.log('\n');
// 显示最近的商品发布请求及其毛重处理
console.log(`找到 ${publishRequestRecords.length} 条商品发布请求记录`);
if (publishRequestRecords.length > 0) {
console.log('\n最近的商品发布请求及毛重处理详情:');
const latestPublish = publishRequestRecords[publishRequestRecords.length - 1];
console.log(`\n时间: ${latestPublish.timestamp}`);
console.log(`起始行号: ${latestPublish.line}`);
console.log('详细内容:');
// 解析请求体中的grossWeight值
const requestBodyMatch = latestPublish.content.match(/请求体: \{([\s\S]*?)\}/);
if (requestBodyMatch) {
console.log(requestBodyMatch[0]);
// 提取grossWeight值
const grossWeightMatch = requestBodyMatch[0].match(/"grossWeight"\s*:\s*(null|\d+(\.\d+)?)/);
if (grossWeightMatch) {
console.log(`\n请求中的毛重值: ${grossWeightMatch[1]}`);
}
}
// 查找毛重处理的相关日志
const grossWeightProcessingMatch = latestPublish.content.match(/\[发布商品.*\] 原始毛重值:.*|毛重值.*设置为.*|最终处理的毛重值:.*|grossWeightStored:.*|毛重.*转换为数字/);
if (grossWeightProcessingMatch) {
console.log('\n毛重处理过程:');
grossWeightProcessingMatch.forEach(processingLine => {
console.log(processingLine);
});
}
}
// 生成总结
console.log('\n===== 分析总结 =====');
if (grossWeightRecords.length === 0) {
console.log('在最近24小时内没有找到与grossWeight相关的日志记录。');
} else {
console.log(`在最近24小时内找到了 ${grossWeightRecords.length} 条与grossWeight相关的日志记录。`);
// 简单统计
const nullGrossWeightCount = grossWeightRecords.filter(r => r.content.includes('grossWeight: null')).length;
const zeroGrossWeightCount = grossWeightRecords.filter(r => r.content.includes('grossWeight: 0')).length;
const numericGrossWeightCount = grossWeightRecords.filter(r => /grossWeight:\s*\d+\.\d+/.test(r.content)).length;
console.log(`- 毛重为null的记录数: ${nullGrossWeightCount}`);
console.log(`- 毛重为0的记录数: ${zeroGrossWeightCount}`);
console.log(`- 毛重为数字的记录数: ${numericGrossWeightCount}`);
}
console.log('\n提示: 如果需要查看更多详细信息,建议直接查看日志文件或使用更专业的日志分析工具。');
} catch (error) {
console.error('分析日志时发生错误:', error.message);
console.log('\n建议手动查看日志文件:');
console.log(`1. 打开文件: ${logFilePath}`);
console.log('2. 搜索关键词: grossWeight');
console.log('3. 特别关注: 发布商品请求中的grossWeight值和处理过程');
}
}
// 执行分析
analyzeGrossWeightLogs();

67
server-example/list-users.js

@ -0,0 +1,67 @@
require('dotenv').config();
const mysql = require('mysql2/promise');
// 查询用户信息
async function listUsers() {
let connection = null;
try {
console.log('连接数据库...');
connection = await mysql.createConnection({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD === undefined ? null : process.env.DB_PASSWORD,
database: process.env.DB_DATABASE || 'wechat_app',
port: process.env.DB_PORT || 3306,
timezone: '+08:00' // 设置时区为UTC+8
});
console.log('数据库连接成功\n');
// 查询users表结构
console.log('=== users表结构 ===');
const [columns] = await connection.execute(
'SHOW COLUMNS FROM users'
);
console.log('字段列表:', columns.map(col => col.Field).join(', '));
console.log();
// 查询前5个用户数据
console.log('=== 用户数据 (前5个) ===');
const [users] = await connection.execute(
'SELECT * FROM users LIMIT 5'
);
console.log(`数据库中有 ${users.length} 个用户记录`);
if (users.length > 0) {
users.forEach((user, index) => {
console.log(`${index + 1}. 用户数据:`, user);
});
// 查找可能的openid字段
const firstUser = users[0];
const possibleOpenIdFields = Object.keys(firstUser).filter(key =>
key.toLowerCase().includes('openid') || key.toLowerCase().includes('open_id')
);
if (possibleOpenIdFields.length > 0) {
console.log('\n=== 可能的openid字段 ===');
console.log('找到以下可能的openid字段:', possibleOpenIdFields.join(', '));
console.log('请使用其中一个有效的字段更新测试脚本');
} else {
console.log('\n未找到明显的openid字段,请检查users表结构后手动更新测试脚本');
}
} else {
console.log('\n数据库中没有用户记录');
}
} catch (error) {
console.error('查询过程中发生错误:', error);
} finally {
if (connection) {
await connection.end();
console.log('\n数据库连接已关闭');
}
}
}
// 运行查询
listUsers();

86
server-example/logger.js

@ -0,0 +1,86 @@
// 日志记录模块,用于将控制台输出同时保存到文件
const fs = require('fs');
const path = require('path');
// 确保logs目录存在
const logsDir = path.join(__dirname, 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir);
}
// 获取当前日期,用于日志文件名
function getCurrentDate() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 获取带时区的时间戳
function getFormattedTimestamp() {
const now = new Date();
const beijingTime = new Date(now.getTime() + 8 * 60 * 60 * 1000);
return beijingTime.toISOString().replace('Z', '+08:00');
}
// 写入日志到文件
function writeLogToFile(level, message) {
const timestamp = getFormattedTimestamp();
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
const logFilePath = path.join(logsDir, `server-${getCurrentDate()}.log`);
fs.appendFile(logFilePath, logMessage, (err) => {
if (err) {
console.error('写入日志文件失败:', err);
}
});
}
// 重写console.log,使其同时输出到控制台和文件
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
const originalConsoleInfo = console.info;
console.log = function (...args) {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
originalConsoleLog.apply(console, args);
writeLogToFile('INFO', message);
};
console.error = function (...args) {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
originalConsoleError.apply(console, args);
writeLogToFile('ERROR', message);
};
console.warn = function (...args) {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
originalConsoleWarn.apply(console, args);
writeLogToFile('WARN', message);
};
console.info = function (...args) {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
originalConsoleInfo.apply(console, args);
writeLogToFile('INFO', message);
};
module.exports = {
logsDir,
getFormattedTimestamp,
writeLogToFile
};

8
server-example/oss-config.js

@ -0,0 +1,8 @@
// 阿里云OSS配置
module.exports = {
region: 'oss-cn-chengdu', // OSS区域,例如 'oss-cn-hangzhou'
accessKeyId: 'LTAI5tRT6ReeHUdmqFpmLZi7', // 访问密钥ID
accessKeySecret: 'zTnK27IAphwgCDMmyJzMUsHYxGsDBE', // 访问密钥Secret
bucket: 'my-supplier-photos', // OSS存储桶名称
endpoint: 'oss-cn-chengdu.aliyuncs.com' // 注意:不要在endpoint中包含bucket名称
};

319
server-example/oss-uploader.js

@ -0,0 +1,319 @@
const fs = require('fs');
const path = require('path');
const { createHash } = require('crypto');
const OSSClient = require('ali-oss');
const ossConfig = require('./oss-config');
// 创建OSS客户端 - ali-oss 6.23.0版本的正确配置
let client = null;
// 初始化OSS客户端的函数
function initOSSClient() {
try {
console.log('初始化OSS客户端配置:', {
region: ossConfig.region,
accessKeyId: ossConfig.accessKeyId ? '已配置' : '未配置',
accessKeySecret: ossConfig.accessKeySecret ? '已配置' : '未配置',
bucket: ossConfig.bucket,
endpoint: `https://${ossConfig.endpoint}`
});
client = new OSSClient({
region: ossConfig.region,
accessKeyId: ossConfig.accessKeyId,
accessKeySecret: ossConfig.accessKeySecret,
bucket: ossConfig.bucket,
endpoint: `https://${ossConfig.endpoint}`, // 添加https协议前缀
cname: false // 对于标准OSS域名,不需要启用cname模式
});
return client;
} catch (error) {
console.error('初始化OSS客户端失败:', error);
throw error;
}
}
// 延迟初始化,避免应用启动时就连接OSS
function getOSSClient() {
if (!client) {
return initOSSClient();
}
return client;
}
class OssUploader {
/**
* 上传文件到OSS
* @param {String} filePath - 本地文件路径
* @param {String} folder - OSS上的文件夹路径
* @param {String} fileType - 文件类型默认为'image'
* @returns {Promise<String>} - 上传后的文件URL
*/
/**
* 计算文件的MD5哈希值
* @param {String} filePath - 文件路径
* @returns {Promise<String>} - MD5哈希值
*/
static async getFileHash(filePath) {
return new Promise((resolve, reject) => {
const hash = createHash('md5');
const stream = fs.createReadStream(filePath);
stream.on('error', reject);
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
});
}
/**
* 计算缓冲区的MD5哈希值
* @param {Buffer} buffer - 数据缓冲区
* @returns {String} - MD5哈希值
*/
static getBufferHash(buffer) {
return createHash('md5').update(buffer).digest('hex');
}
static async uploadFile(filePath, folder = 'images', fileType = 'image') {
try {
console.log('【OSS上传】开始上传文件:', filePath, '到目录:', folder);
// 确保文件存在
const fileExists = await fs.promises.access(filePath).then(() => true).catch(() => false);
if (!fileExists) {
throw new Error(`文件不存在: ${filePath}`);
}
// 获取文件扩展名
const extname = path.extname(filePath).toLowerCase();
if (!extname) {
throw new Error(`无法获取文件扩展名: ${filePath}`);
}
// 基于文件内容计算MD5哈希值,实现文件级去重
console.log('【文件去重】开始计算文件哈希值...');
const fileHash = await this.getFileHash(filePath);
console.log(`【文件去重】文件哈希计算完成: ${fileHash}`);
// 使用哈希值作为文件名,确保相同内容的文件生成相同的文件名
const uniqueFilename = `${fileHash}${extname}`;
const ossFilePath = `${folder}/${fileType}/${uniqueFilename}`;
console.log(`【文件去重】使用基于内容的文件名: ${uniqueFilename}`);
// 获取OSS客户端,延迟初始化
const ossClient = getOSSClient();
// 测试OSS连接
try {
await ossClient.list({ max: 1 });
console.log('OSS连接测试成功');
} catch (connError) {
console.error('OSS连接测试失败,尝试重新初始化客户端:', connError.message);
// 尝试重新初始化客户端
initOSSClient();
}
// 检查OSS客户端配置
console.log('【OSS上传】OSS配置检查 - region:', ossClient.options.region, 'bucket:', ossClient.options.bucket);
// 上传文件,明确设置为公共读权限
console.log(`开始上传文件到OSS: ${filePath} -> ${ossFilePath}`);
const result = await ossClient.put(ossFilePath, filePath, {
headers: {
'x-oss-object-acl': 'public-read' // 确保文件可以公开访问
},
acl: 'public-read' // 额外设置ACL参数,确保文件公开可读
});
console.log(`文件上传成功: ${result.url}`);
console.log('已设置文件为公共读权限');
// 返回完整的文件URL
return result.url;
} catch (error) {
console.error('【OSS上传】上传文件失败:', error);
console.error('【OSS上传】错误详情:', error.message);
console.error('【OSS上传】错误栈:', error.stack);
throw error;
}
}
/**
* 从缓冲区上传文件到OSS
* @param {Buffer} buffer - 文件数据缓冲区
* @param {String} filename - 文件名
* @param {String} folder - OSS上的文件夹路径
* @param {String} fileType - 文件类型默认为'image'
* @returns {Promise<String>} - 上传后的文件URL
*/
static async uploadBuffer(buffer, filename, folder = 'images', fileType = 'image') {
try {
// 获取文件扩展名
const extname = path.extname(filename).toLowerCase();
if (!extname) {
throw new Error(`无法获取文件扩展名: ${filename}`);
}
// 基于文件内容计算MD5哈希值,实现文件级去重
console.log('【文件去重】开始计算缓冲区哈希值...');
const bufferHash = this.getBufferHash(buffer);
console.log(`【文件去重】缓冲区哈希计算完成: ${bufferHash}`);
// 使用哈希值作为文件名,确保相同内容的文件生成相同的文件名
const uniqueFilename = `${bufferHash}${extname}`;
const ossFilePath = `${folder}/${fileType}/${uniqueFilename}`;
console.log(`【文件去重】使用基于内容的文件名: ${uniqueFilename}`);
// 获取OSS客户端,延迟初始化
const ossClient = getOSSClient();
// 上传缓冲区,明确设置为公共读权限
console.log(`开始上传缓冲区到OSS: ${ossFilePath}`);
const result = await ossClient.put(ossFilePath, buffer, {
headers: {
'x-oss-object-acl': 'public-read' // 确保文件可以公开访问
},
acl: 'public-read' // 额外设置ACL参数,确保文件公开可读
});
console.log(`缓冲区上传成功: ${result.url}`);
console.log('已设置文件为公共读权限');
// 返回完整的文件URL
return result.url;
} catch (error) {
console.error('OSS缓冲区上传失败:', error);
console.error('OSS缓冲区上传错误详情:', error.message);
console.error('OSS缓冲区上传错误栈:', error.stack);
throw error;
}
}
/**
* 批量上传文件到OSS
* @param {Array<String>} filePaths - 本地文件路径数组
* @param {String} folder - OSS上的文件夹路径
* @param {String} fileType - 文件类型默认为'image'
* @returns {Promise<Array<String>>} - 上传后的文件URL数组
*/
static async uploadFiles(filePaths, folder = 'images', fileType = 'image') {
try {
const uploadPromises = filePaths.map(filePath =>
this.uploadFile(filePath, folder, fileType)
);
const urls = await Promise.all(uploadPromises);
console.log(`批量上传完成,成功上传${urls.length}个文件`);
return urls;
} catch (error) {
console.error('OSS批量上传失败:', error);
throw error;
}
}
/**
* 删除OSS上的文件
* @param {String} ossFilePath - OSS上的文件路径
* @returns {Promise<Boolean>} - 删除是否成功
*/
static async deleteFile(ossFilePath) {
try {
console.log(`【OSS删除】开始删除OSS文件: ${ossFilePath}`);
const ossClient = getOSSClient();
// 【新增】记录OSS客户端配置信息(隐藏敏感信息)
console.log(`【OSS删除】OSS客户端配置 - region: ${ossClient.options.region}, bucket: ${ossClient.options.bucket}`);
const result = await ossClient.delete(ossFilePath);
console.log(`【OSS删除】OSS文件删除成功: ${ossFilePath}`, result);
return true;
} catch (error) {
console.error('【OSS删除】OSS文件删除失败:', ossFilePath, '错误:', error.message);
console.error('【OSS删除】错误详情:', error);
// 【增强日志】详细分析错误类型
console.log('【OSS删除】=== 错误详细分析开始 ===');
console.log('【OSS删除】错误名称:', error.name);
console.log('【OSS删除】错误代码:', error.code);
console.log('【OSS删除】HTTP状态码:', error.status);
console.log('【OSS删除】请求ID:', error.requestId);
console.log('【OSS删除】主机ID:', error.hostId);
// 【关键检查】判断是否为权限不足错误
const isPermissionError =
error.code === 'AccessDenied' ||
error.status === 403 ||
error.message.includes('permission') ||
error.message.includes('AccessDenied') ||
error.message.includes('do not have write permission');
if (isPermissionError) {
console.error('【OSS删除】❌ 确认是权限不足错误!');
console.error('【OSS删除】❌ 当前AccessKey缺少删除文件的权限');
console.error('【OSS删除】❌ 请检查RAM策略是否包含 oss:DeleteObject 权限');
console.error('【OSS删除】❌ 建议在RAM中授予 AliyunOSSFullAccess 或自定义删除权限');
}
console.log('【OSS删除】=== 错误详细分析结束 ===');
// 如果文件不存在,也算删除成功
if (error.code === 'NoSuchKey' || error.status === 404) {
console.log(`【OSS删除】文件不存在,视为删除成功: ${ossFilePath}`);
return true;
}
// 【新增】对于权限错误,提供更友好的错误信息
if (isPermissionError) {
const permissionError = new Error(`OSS删除权限不足: ${error.message}`);
permissionError.code = 'OSS_ACCESS_DENIED';
permissionError.originalError = error;
throw permissionError;
}
throw error;
}
}
/**
* 获取OSS配置信息
* @returns {Object} - OSS配置信息
*/
static getConfig() {
return {
region: ossConfig.region,
bucket: ossConfig.bucket,
endpoint: ossConfig.endpoint
};
}
/**
* 测试OSS连接
* @returns {Promise<Object>} - 连接测试结果
*/
static async testConnection() {
try {
console.log('【OSS连接测试】开始测试OSS连接...');
const ossClient = getOSSClient();
// 执行简单的list操作来验证连接
const result = await ossClient.list({ max: 1 });
console.log('【OSS连接测试】连接成功,存储桶中有', result.objects.length, '个对象');
return {
success: true,
message: 'OSS连接成功',
region: ossClient.options.region,
bucket: ossClient.options.bucket
};
} catch (error) {
console.error('【OSS连接测试】连接失败:', error.message);
return {
success: false,
message: `OSS连接失败: ${error.message}`,
error: error.message
};
}
}
}
module.exports = OssUploader;

2393
server-example/package-lock.json

File diff suppressed because it is too large

35
server-example/package.json

@ -0,0 +1,35 @@
{
"name": "wechat-miniprogram-server",
"version": "1.0.0",
"description": "微信小程序服务器示例",
"main": "server-mysql.js",
"scripts": {
"start": "node start-server.js",
"dev": "nodemon server-mysql.js",
"logs-pm2": "pm2 logs wechat-app",
"view-logs": "powershell \"Get-Content logs/server-*.log -Tail 100\"",
"view-output-log": "powershell \"Get-Content logs/output.log -Tail 100\"",
"monitor-logs": "powershell \"Get-Content logs/server-*.log -Wait -Tail 50\"",
"restart-pm2": "pm2 restart ecosystem.config.js",
"start-pm2": "pm2 start ecosystem.config.js",
"stop-pm2": "pm2 stop ecosystem.config.js",
"view-pm2-status": "pm2 status",
"install-pm2-startup": "npm install -g pm2-windows-startup && pm2-startup install",
"save-pm2-config": "pm2 save",
"setup-autostart": "npm run start-pm2 && npm run save-pm2-config"
},
"dependencies": {
"ali-oss": "^6.23.0",
"axios": "^1.13.2",
"body-parser": "^1.20.2",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"form-data": "^4.0.4",
"multer": "^2.0.2",
"mysql2": "^3.6.5",
"sequelize": "^6.35.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

236
server-example/port-conflict-fix.js

@ -0,0 +1,236 @@
#!/usr/bin/env node
const { execSync, exec } = require('child_process');
const fs = require('fs');
const path = require('path');
// 检查端口是否被占用
function isPortTaken(port) {
return new Promise((resolve) => {
// 在Linux上使用lsof检查端口占用
try {
execSync(`lsof -i :${port}`, { stdio: 'ignore' });
resolve(true);
} catch (error) {
resolve(false);
}
});
}
// 查找占用端口的进程ID
function findProcessUsingPort(port) {
try {
const output = execSync(`lsof -i :${port} | grep LISTEN`, { encoding: 'utf8' });
const lines = output.trim().split('\n');
if (lines.length > 0) {
const parts = lines[0].trim().split(/\s+/);
return { pid: parts[1], process: parts[0], user: parts[2] };
}
} catch (error) {
console.log(`未找到占用端口 ${port} 的进程`);
}
return null;
}
// 停止指定进程
function stopProcess(pid) {
try {
execSync(`kill -9 ${pid}`);
console.log(`成功停止进程 ${pid}`);
return true;
} catch (error) {
console.error(`停止进程 ${pid} 失败:`, error.message);
return false;
}
}
// 修改PM2配置文件中的端口
function updatePM2ConfigFile(newPort) {
const configPath = path.join(__dirname, 'ecosystem.config.js');
try {
let content = fs.readFileSync(configPath, 'utf8');
// 备份配置文件
const backupPath = configPath + '.bak.' + Date.now();
fs.writeFileSync(backupPath, content);
console.log(`已创建配置备份: ${backupPath}`);
// 修改production环境端口
content = content.replace(/PORT:\s*3001/g, `PORT: ${newPort}`);
fs.writeFileSync(configPath, content);
console.log(`已将PM2配置中的端口修改为: ${newPort}`);
return true;
} catch (error) {
console.error('修改PM2配置文件失败:', error.message);
return false;
}
}
// 重启PM2应用
function restartPM2App() {
try {
execSync('pm2 restart ecosystem.config.js', { stdio: 'inherit' });
return true;
} catch (error) {
console.error('重启PM2应用失败:', error.message);
return false;
}
}
// 主函数
async function main() {
console.log('=== 微信小程序后端服务 - 端口冲突修复工具 ===\n');
const originalPort = 3001;
let newPort = 3001;
// 检查原始端口是否被占用
const isOriginalPortTaken = await isPortTaken(originalPort);
if (isOriginalPortTaken) {
console.log(`发现端口 ${originalPort} 被占用!`);
// 查找占用进程
const processInfo = findProcessUsingPort(originalPort);
if (processInfo) {
console.log(`占用端口的进程信息:`);
console.log(`- 进程ID: ${processInfo.pid}`);
console.log(`- 进程名称: ${processInfo.process}`);
console.log(`- 用户: ${processInfo.user}`);
// 询问是否停止该进程
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
readline.question(`是否停止该进程以释放端口 ${originalPort}? (y/n): `, (answer) => {
readline.close();
if (answer.toLowerCase() === 'y') {
if (stopProcess(processInfo.pid)) {
console.log(`端口 ${originalPort} 已被释放,正在重启应用...`);
restartPM2App();
}
} else {
// 查找可用端口
console.log('正在查找可用端口...');
let foundPort = false;
for (let i = 3002; i <= 3100; i++) {
execSync(`lsof -i :${i}`, { stdio: 'ignore' }, (error) => {
if (error) {
newPort = i;
foundPort = true;
// 修改PM2配置并重启
if (updatePM2ConfigFile(newPort)) {
console.log(`已切换到可用端口: ${newPort}`);
console.log('正在重启PM2应用...');
restartPM2App();
}
}
});
if (foundPort) break;
}
if (!foundPort) {
console.error('未找到可用端口,请手动指定一个未被占用的端口。');
}
}
});
} else {
console.error('无法确定占用端口的进程,请手动检查端口占用情况。');
}
} else {
console.log(`端口 ${originalPort} 未被占用,检查应用状态...`);
// 检查PM2应用状态
try {
const statusOutput = execSync('pm2 status wechat-app', { encoding: 'utf8' });
console.log(statusOutput);
// 提示用户重启应用
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
readline.question('是否需要重启应用? (y/n): ', (answer) => {
readline.close();
if (answer.toLowerCase() === 'y') {
restartPM2App();
} else {
console.log('操作已取消。');
}
});
} catch (error) {
console.error('检查PM2应用状态失败:', error.message);
console.log('建议尝试手动重启应用: pm2 restart wechat-app');
}
}
}
// 提供非交互式修复选项
function provideNonInteractiveFix() {
console.log('\n=== 非交互式修复选项 ===');
console.log('1. 强制释放3001端口并重启应用');
console.log('2. 使用备用端口3004并更新配置');
console.log('3. 查看当前端口占用情况');
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
readline.question('请选择修复方式 (1-3): ', (answer) => {
readline.close();
switch (answer) {
case '1':
const processInfo = findProcessUsingPort(3001);
if (processInfo && stopProcess(processInfo.pid)) {
console.log('正在重启应用...');
restartPM2App();
} else {
console.log('端口未被占用或无法停止占用进程');
}
break;
case '2':
if (updatePM2ConfigFile(3004)) {
console.log('正在重启应用...');
restartPM2App();
}
break;
case '3':
try {
console.log('端口占用情况:');
execSync('netstat -tuln | grep 300', { stdio: 'inherit' });
} catch (error) {
console.log('未找到相关端口占用信息');
}
break;
default:
console.log('无效选项,操作已取消。');
}
});
}
// 运行主程序
main().catch(err => {
console.error('修复过程中发生错误:', err);
provideNonInteractiveFix();
});
// 提供帮助信息
setTimeout(() => {
console.log('\n如果自动修复失败,可以尝试以下手动解决方案:');
console.log('1. 检查端口占用: lsof -i :3001 或 netstat -ano | findstr :3001');
console.log('2. 停止占用进程: kill -9 [进程ID]');
console.log('3. 或者修改PM2配置使用其他端口:');
console.log(' - 编辑 ecosystem.config.js 文件');
console.log(' - 将 PORT: 3001 修改为其他可用端口');
console.log(' - 保存并运行: pm2 restart ecosystem.config.js');
}, 1000);

71
server-example/query-database.js

@ -0,0 +1,71 @@
// 查询数据库中的用户和商品信息
require('dotenv').config();
const { Sequelize } = require('sequelize');
// 创建数据库连接
const sequelize = new Sequelize(
process.env.DB_DATABASE || 'wechat_app',
process.env.DB_USER || 'root',
process.env.DB_PASSWORD === undefined ? null : process.env.DB_PASSWORD,
{
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306,
dialect: 'mysql',
timezone: '+08:00' // 设置时区为UTC+8
}
);
// 执行查询
async function queryDatabase() {
try {
// 测试连接
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
// 查询用户信息
const users = await sequelize.query('SELECT * FROM users LIMIT 10', { type: sequelize.QueryTypes.SELECT });
console.log('\n👥 用户列表:');
console.log(users.map(u => ({
id: u.id,
openid: u.openid,
userId: u.userId,
type: u.type
})));
// 查询商品信息,特别是拒绝状态的商品
const products = await sequelize.query(
'SELECT productId, sellerId, productName, status, rejectReason, created_at FROM products LIMIT 20',
{ type: sequelize.QueryTypes.SELECT }
);
console.log('\n🛒 商品列表:');
console.log(products.map(p => ({
productId: p.productId,
sellerId: p.sellerId,
productName: p.productName,
status: p.status,
rejectReason: p.rejectReason,
created_at: p.created_at
})));
// 特别列出拒绝状态的商品
const rejectedProducts = products.filter(p => p.status === 'rejected');
console.log('\n❌ 审核拒绝的商品:');
console.log(rejectedProducts.map(p => ({
productId: p.productId,
sellerId: p.sellerId,
productName: p.productName,
status: p.status,
rejectReason: p.rejectReason
})));
} catch (error) {
console.error('❌ 查询失败:', error.message);
} finally {
// 关闭连接
await sequelize.close();
}
}
// 运行查询
queryDatabase();

2973
server-example/server-mysql-backup-alias.js

File diff suppressed because it is too large

2973
server-example/server-mysql-backup-count.js

File diff suppressed because it is too large

2973
server-example/server-mysql-backup-final.js

File diff suppressed because it is too large

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save