This commit is contained in:
sjk
2026-01-07 22:55:12 +08:00
parent cb267e8d5e
commit 4720ab2a15
76 changed files with 3110 additions and 7168 deletions

View File

@@ -1,24 +1,15 @@
{
"pages": [
"pages/home/home",
"pages/article-generate/article-generate",
"pages/login/login",
"pages/login/phone-login",
"pages/articles/articles",
"pages/article-detail/article-detail",
"pages/profile/profile",
"pages/profile/user-info/user-info",
"pages/profile/social-binding/social-binding",
"pages/profile/platform-bind/platform-bind",
"pages/profile/xhs-login/xhs-login",
"pages/profile/published/published",
"pages/profile/article-detail/article-detail",
"pages/profile/about/about",
"pages/profile/feedback/feedback",
"pages/agreement/user-agreement/user-agreement",
"pages/agreement/privacy-policy/privacy-policy",
"pages/index/index",
"pages/logs/logs"
"pages/agreement/privacy-policy/privacy-policy"
],
"window": {
"navigationBarTextStyle": "white",

View File

@@ -56,14 +56,6 @@ Page({
this.setData({
showClaimButton: false
});
// 延迟后跳转到发布页面,传递领取信息
setTimeout(() => {
const copy = response.data!.copy;
wx.redirectTo({
url: `/pages/article-generate/article-generate?copyId=${copyId}&claimId=${response.data!.claim_id}&productId=${productId}&productName=${encodeURIComponent(copy.title)}&title=${encodeURIComponent(copy.title)}&content=${encodeURIComponent(copy.content)}`
});
}, 1500);
}
} catch (error) {
console.error('领取文案失败:', error);

View File

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

View File

@@ -1,54 +0,0 @@
// index.ts
// 获取应用实例
const app = getApp<IAppOption>()
const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
Component({
data: {
motto: 'Hello World',
userInfo: {
avatarUrl: defaultAvatarUrl,
nickName: '',
},
hasUserInfo: false,
canIUseGetUserProfile: wx.canIUse('getUserProfile'),
canIUseNicknameComp: wx.canIUse('input.type.nickname'),
},
methods: {
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '../logs/logs',
})
},
onChooseAvatar(e: any) {
const { avatarUrl } = e.detail
const { nickName } = this.data.userInfo
this.setData({
"userInfo.avatarUrl": avatarUrl,
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
})
},
onInputChange(e: any) {
const nickName = e.detail.value
const { avatarUrl } = this.data.userInfo
this.setData({
"userInfo.nickName": nickName,
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
})
},
getUserProfile() {
// 推荐使用wx.getUserProfile获取用户信息开发者每次通过该接口获取用户个人信息均需用户确认开发者妥善保管用户快速填写的头像昵称避免重复弹窗
wx.getUserProfile({
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
console.log(res)
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
},
},
})

View File

@@ -1,27 +0,0 @@
<!--index.wxml-->
<scroll-view class="scrollarea" scroll-y type="list">
<view class="container">
<view class="userinfo">
<block wx:if="{{canIUseNicknameComp && !hasUserInfo}}">
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
</button>
<view class="nickname-wrapper">
<text class="nickname-label">昵称</text>
<input type="nickname" class="nickname-input" placeholder="请输入昵称" bind:change="onInputChange" />
</view>
</block>
<block wx:elif="{{!hasUserInfo}}">
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
<view wx:else> 请使用2.10.4及以上版本基础库 </view>
</block>
<block wx:else>
<image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</block>
</view>
<view class="usermotto">
<text class="user-motto">{{motto}}</text>
</view>
</view>
</scroll-view>

View File

@@ -1,62 +0,0 @@
/**index.wxss**/
page {
height: 100vh;
display: flex;
flex-direction: column;
}
.scrollarea {
flex: 1;
overflow-y: hidden;
}
.userinfo {
display: flex;
flex-direction: column;
align-items: center;
color: #aaa;
width: 80%;
}
.userinfo-avatar {
overflow: hidden;
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
}
.usermotto {
margin-top: 200px;
}
.avatar-wrapper {
padding: 0;
width: 56px !important;
border-radius: 8px;
margin-top: 40px;
margin-bottom: 40px;
}
.avatar {
display: block;
width: 56px;
height: 56px;
}
.nickname-wrapper {
display: flex;
width: 100%;
padding: 16px;
box-sizing: border-box;
border-top: .5px solid rgba(0, 0, 0, 0.1);
border-bottom: .5px solid rgba(0, 0, 0, 0.1);
color: black;
}
.nickname-label {
width: 105px;
}
.nickname-input {
flex: 1;
}

View File

@@ -1,5 +1,6 @@
// pages/login/phone-login.ts
import { API } from '../../config/api';
import { EmployeeService } from '../../services/employee';
Page({
data: {
@@ -143,7 +144,7 @@ Page({
},
// 获取验证码
getVerifyCode() {
async getVerifyCode() {
if (!this.data.agreed) {
wx.showToast({
title: '请先同意用户协议',
@@ -167,56 +168,42 @@ Page({
return;
}
// 显示加载提示
wx.showLoading({
title: '发送中...',
mask: true
});
// 调用后端API发送验证码
wx.request({
url: `${API.baseURL}/api/xhs/send-verification-code`,
method: 'POST',
data: {
phone: phone
},
success: (res: any) => {
wx.hideLoading();
if (res.statusCode === 200 && res.data.code === 200) {
// 发送成功
wx.showToast({
title: '验证码已发送',
icon: 'success',
duration: 2000
});
// 开发环境打印验证码
if (res.data.data && res.data.data.code) {
console.log('验证码:', res.data.data.code);
}
// 开始倒计时
this.startCountdown();
} else {
// 发送失败
wx.showToast({
title: res.data.message || '发送失败,请稍后重试',
icon: 'none',
duration: 2000
});
}
},
fail: (err) => {
wx.hideLoading();
console.error('发送验证码请求失败:', err);
try {
// 调用封装的Service方法发送验证码禁用loading验证码发送应立即响应
const res = await EmployeeService.sendXHSCode(phone, false);
// 兼容 code=0 和 code=200
if (res.code === 200 || res.code === 0) {
// 发送成功
wx.showToast({
title: '网络错误,请稍后重试',
title: '验证码已发送',
icon: 'success',
duration: 2000
});
// 开发环境打印验证码
if (res.data && res.data.code) {
console.log('验证码:', res.data.code);
}
// 开始倒计时
this.startCountdown();
} else {
// 发送失败
wx.showToast({
title: res.message || '发送失败,请稍后重试',
icon: 'none',
duration: 2000
});
}
});
} catch (err) {
console.error('发送验证码失败:', err);
wx.showToast({
title: '网络错误,请稍后重试',
icon: 'none',
duration: 2000
});
}
},
// 开始倒计时

View File

@@ -1,5 +0,0 @@
{
"navigationBarTitleText": "关于我们",
"navigationBarBackgroundColor": "#ff2442",
"navigationBarTextStyle": "white"
}

View File

@@ -1,10 +0,0 @@
// pages/profile/about/about.ts
Page({
data: {
},
onLoad() {
}
});

View File

@@ -1,45 +0,0 @@
<!--pages/profile/about/about.wxml-->
<view class="page-container">
<view class="logo-section">
<image class="app-logo" src="/images/logo.png" mode="aspectFit"></image>
<text class="app-name">AI文章审核平台</text>
<text class="app-version">v1.0.0</text>
</view>
<view class="info-section">
<view class="info-card">
<view class="card-title">产品介绍</view>
<text class="card-content">AI文章审核平台是一款智能内容生成与审核管理系统通过AI技术帮助用户高效管理文章内容提升内容审核效率。</text>
</view>
<view class="info-card">
<view class="card-title">联系我们</view>
<view class="contact-item">
<text class="contact-label">客服邮箱:</text>
<text class="contact-value">support@example.com</text>
</view>
<view class="contact-item">
<text class="contact-label">客服电话:</text>
<text class="contact-value">400-888-8888</text>
</view>
<view class="contact-item">
<text class="contact-label">工作时间:</text>
<text class="contact-value">周一至周五 9:00-18:00</text>
</view>
</view>
<view class="info-card">
<view class="card-title">更新日志</view>
<view class="log-item">
<text class="log-version">v1.0.0</text>
<text class="log-date">2024-12-05</text>
<text class="log-content">· 初始版本发布\n· 支持文章管理和审核\n· 优化用户体验</text>
</view>
</view>
</view>
<view class="footer-section">
<text class="copyright">© 2024 AI文章审核平台</text>
<text class="copyright">All Rights Reserved</text>
</view>
</view>

View File

@@ -1,144 +0,0 @@
/* pages/profile/about/about.wxss */
page {
background: #f8f8f8;
height: 100%;
width: 100%;
}
.page-container {
min-height: 100vh;
width: 100%;
padding-bottom: 60rpx;
box-sizing: border-box;
overflow-x: hidden;
}
.logo-section {
width: 100%;
background: #07c160;
padding: 80rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}
.app-logo {
width: 160rpx;
height: 160rpx;
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
margin-bottom: 30rpx;
}
.app-name {
font-size: 40rpx;
font-weight: bold;
color: white;
margin-bottom: 16rpx;
}
.app-version {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.info-section {
width: 100%;
padding: 20rpx 30rpx;
box-sizing: border-box;
}
.info-card {
width: 100%;
background: white;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
box-sizing: border-box;
word-wrap: break-word;
word-break: break-all;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.card-content {
font-size: 28rpx;
color: #666;
line-height: 1.8;
word-wrap: break-word;
word-break: break-all;
}
.contact-item {
display: flex;
margin-bottom: 16rpx;
word-wrap: break-word;
word-break: break-all;
}
.contact-item:last-child {
margin-bottom: 0;
}
.contact-label {
font-size: 28rpx;
color: #999;
min-width: 140rpx;
}
.contact-value {
font-size: 28rpx;
color: #333;
word-wrap: break-word;
word-break: break-all;
flex: 1;
}
.log-item {
display: flex;
flex-direction: column;
}
.log-version {
font-size: 30rpx;
font-weight: 600;
color: #07c160;
margin-bottom: 8rpx;
}
.log-date {
font-size: 24rpx;
color: #999;
margin-bottom: 16rpx;
}
.log-content {
font-size: 28rpx;
color: #666;
line-height: 1.8;
white-space: pre-line;
}
.footer-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 30rpx;
box-sizing: border-box;
}
.copyright {
font-size: 24rpx;
color: #999;
line-height: 1.6;
}

View File

@@ -1,5 +0,0 @@
{
"navigationBarTitleText": "环境切换",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white"
}

View File

@@ -1,62 +0,0 @@
<!--pages/profile/env-switch/env-switch.wxml-->
<view class="container">
<!-- 当前环境 -->
<view class="current-env">
<view class="env-label">当前环境</view>
<view class="env-value">
<text class="env-tag env-tag-{{currentEnv}}">{{currentEnv === 'dev' ? '开发环境' : currentEnv === 'test' ? '测试环境' : '生产环境'}}</text>
</view>
</view>
<!-- 环境列表 -->
<view class="env-list">
<view
class="env-item {{currentEnv === item.key ? 'active' : ''}}"
wx:for="{{envList}}"
wx:key="key"
bindtap="switchEnvironment"
data-env="{{item.key}}"
>
<view class="env-header">
<view class="env-name">
<text class="env-icon" style="background-color: {{item.color}}"></text>
<text>{{item.name}}</text>
</view>
<view class="env-status">
<text wx:if="{{currentEnv === item.key}}" class="current-tag">当前</text>
<text wx:else class="switch-text">切换</text>
</view>
</view>
<view class="env-info">
<view class="info-item">
<text class="info-label">主服务:</text>
<text class="info-value" bindtap="copyURL" catchtap="copyURL" data-url="{{configs[item.key].baseURL}}">
{{configs[item.key].baseURL}}
</text>
</view>
<view class="info-item" wx:if="{{configs[item.key].pythonURL}}">
<text class="info-label">Python:</text>
<text class="info-value" bindtap="copyURL" catchtap="copyURL" data-url="{{configs[item.key].pythonURL}}">
{{configs[item.key].pythonURL}}
</text>
</view>
</view>
</view>
</view>
<!-- 说明 -->
<view class="tips">
<view class="tips-title">⚠️ 温馨提示</view>
<view class="tips-item">• 切换环境后会清除登录状态,需要重新登录</view>
<view class="tips-item">• 开发环境用于本地开发调试</view>
<view class="tips-item">• 测试环境用于服务器功能测试</view>
<view class="tips-item">• 生产环境为正式线上环境</view>
<view class="tips-item">• 点击地址可以复制</view>
</view>
<!-- 操作按钮 -->
<view class="actions">
<button class="action-btn" bindtap="restartApp">重启小程序</button>
</view>
</view>

View File

@@ -1,5 +0,0 @@
{
"navigationBarTitleText": "意见反馈",
"navigationBarBackgroundColor": "#07c160",
"navigationBarTextStyle": "white"
}

View File

@@ -1,97 +0,0 @@
// pages/profile/feedback/feedback.ts
import { request } from '../../../utils/request';
Page({
data: {
typeList: ['功能建议', 'Bug反馈', '体验问题', '其他'],
typeIndex: 0,
content: '',
contact: '',
nickname: ''
},
onLoad() {
// 获取用户信息
const employeeInfo = wx.getStorageSync('employeeInfo');
if (employeeInfo && employeeInfo.name) {
this.setData({
nickname: employeeInfo.name
});
}
},
onTypeChange(e: any) {
this.setData({
typeIndex: e.detail.value
});
},
onContentInput(e: any) {
console.log('输入内容:', e.detail.value);
this.setData({
content: e.detail.value
});
},
onContactInput(e: any) {
this.setData({
contact: e.detail.value
});
},
async handleSubmit() {
console.log('提交时的内容:', this.data.content);
console.log('内容长度:', this.data.content.length);
console.log('去空格后:', this.data.content.trim());
if (!this.data.content.trim()) {
wx.showToast({
title: '请输入问题描述',
icon: 'none'
});
return;
}
wx.showLoading({
title: '提交中...',
mask: true
});
try {
const res = await request({
url: '/api/employee/feedback',
method: 'POST',
data: {
feedback_type: this.data.typeList[this.data.typeIndex],
description: this.data.content.trim(),
contact_info: this.data.contact.trim(),
nickname: this.data.nickname
}
});
wx.hideLoading();
if (res.code === 200) {
wx.showToast({
title: '提交成功',
icon: 'success'
});
setTimeout(() => {
wx.navigateBack();
}, 1500);
} else {
wx.showToast({
title: res.message || '提交失败',
icon: 'none'
});
}
} catch (error: any) {
wx.hideLoading();
wx.showToast({
title: error.message || '网络错误',
icon: 'none'
});
}
}
});

View File

@@ -1,38 +0,0 @@
<!--pages/profile/feedback/feedback.wxml-->
<view class="page-container">
<view class="form-section">
<view class="form-item">
<view class="item-label">反馈类型</view>
<picker bindchange="onTypeChange" value="{{typeIndex}}" range="{{typeList}}">
<view class="picker-value">
{{typeList[typeIndex]}}
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<view class="form-item textarea-item">
<view class="item-label">问题描述</view>
<textarea
class="feedback-textarea"
placeholder="请详细描述您遇到的问题或建议"
maxlength="500"
model:value="{{content}}"
/>
<view class="char-count">{{content.length}}/500</view>
</view>
<view class="form-item">
<view class="item-label">联系方式(选填)</view>
<input
class="feedback-input"
placeholder="请输入您的手机号或邮箱"
model:value="{{contact}}"
/>
</view>
</view>
<view class="submit-section">
<button class="submit-btn" bindtap="handleSubmit">提交反馈</button>
</view>
</view>

View File

@@ -1,102 +0,0 @@
/* pages/profile/feedback/feedback.wxss */
page {
background: #f8f8f8;
}
.page-container {
min-height: 100vh;
padding-bottom: 120rpx;
}
.form-section {
width: 100%;
background: white;
margin-top: 20rpx;
}
.form-item {
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.form-item:last-child {
border-bottom: none;
}
.item-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 20rpx;
}
.picker-value {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 28rpx;
color: #666;
padding: 20rpx 0;
}
.picker-arrow {
font-size: 36rpx;
color: #ccc;
}
.feedback-textarea {
width: 100%;
min-height: 300rpx;
font-size: 28rpx;
color: #333;
line-height: 1.6;
padding: 20rpx;
background: #f8f8f8;
border-radius: 8rpx;
box-sizing: border-box;
}
.char-count {
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 12rpx;
}
.feedback-input {
width: 100%;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #333;
padding: 0 20rpx;
background: #f8f8f8;
border-radius: 8rpx;
box-sizing: border-box;
}
.submit-section {
width: 100%;
padding: 40rpx 30rpx;
}
.submit-btn {
width: 100%;
background: #07c160;
color: white;
border: none;
border-radius: 50rpx;
padding: 32rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
}
.submit-btn::after {
border: none;
}
.submit-btn:active {
opacity: 0.9;
transform: translateY(2rpx);
}

View File

@@ -1,5 +1,6 @@
// pages/profile/platform-bind/platform-bind.ts
import { EmployeeService } from '../../../services/employee';
import { API } from '../../../config/api';
Page({
data: {
@@ -19,7 +20,20 @@ Page({
// 验证码相关
needCaptcha: false, // 是否需要验证码
captchaType: '', // 验证码类型
qrcodeImage: '' // 二维码图片base64
qrcodeImage: '', // 二维码图片base64
sessionId: '', // 发送验证码时返回的session_id用于复用浏览器
// 登录方式
loginType: 'phone' as 'phone' | 'qrcode', // phone: 手机号登录, qrcode: 扫码登录
qrcodeSessionId: '', // 扫码登录的session_id
qrcodeExpired: false, // 二维码是否过期
statusText: '', // 状态文本
statusDesc: '', // 状态描述
qrcodePolling: false, // 是否正在轮询二维码状态
qrUrl: '', // 二维码登录链接
qrId: '', // 二维码ID
qrCode: '', // 二维码code
qrcodeError: '', // 二维码加载错误提示
qrcodeLoading: false // 二维码是否正在加载
},
onLoad() {
@@ -83,7 +97,15 @@ Page({
const countryCode = countryCodes[countryCodeIndex];
console.log('发送验证码到:', phone, '区号:', countryCode);
await EmployeeService.sendXHSCode(phone);
const result = await EmployeeService.sendXHSCode(phone);
// 保存session_id用于后续复用浏览器
if (result.data && result.data.session_id) {
this.setData({
sessionId: result.data.session_id
});
console.log('已保存session_id:', result.data.session_id);
}
this.setData({
showLoading: false
@@ -142,7 +164,7 @@ Page({
// 绑定账号
async bindAccount() {
const { phone, code } = this.data;
const { phone, code, sessionId } = this.data;
// 验证手机号
if (!phone || phone.length !== 11) {
@@ -163,6 +185,16 @@ Page({
});
return;
}
// 验证session_id
if (!sessionId) {
wx.showToast({
title: '请重新发送验证码',
icon: 'none',
duration: 2000
});
return;
}
// 显示加载
this.setData({
@@ -173,9 +205,9 @@ Page({
try {
// 调用后端API进行绑定异步处理
console.log('绑定小红书账号:', { phone, code });
console.log('绑定小红书账号:', { phone, code, sessionId });
const result = await EmployeeService.bindXHS(phone, code);
const result = await EmployeeService.bindXHS(phone, code, sessionId);
// 后端立即返回,开始轮询绑定状态
this.setData({
@@ -267,7 +299,7 @@ Page({
showSuccess: true
});
// 保存绑定信息到本地
// 更新本地绑定状态缓存
const bindings = wx.getStorageSync('socialBindings') || {};
bindings.xiaohongshu = {
phone: this.data.phone,
@@ -276,17 +308,17 @@ Page({
cookieExpired: false
};
wx.setStorageSync('socialBindings', bindings);
console.log('[手机号登录] 绑定成功,已更新本地缓存');
setTimeout(() => {
this.setData({
showSuccess: false
});
// 绑定成功后直接跳转到首页
wx.reLaunch({
url: '/pages/home/home'
});
}, 1500);
// 绑定成功后返回个人中心
wx.navigateBack();
}, 2000);
} else if (status.status === 'failed') {
// 绑定失败
@@ -344,5 +376,477 @@ Page({
pollTimer: null
});
}
},
// 切换到手机号登录
switchToPhone() {
// 停止二维码轮询
this.stopQRCodePolling();
// 清空所有扫码登录相关数据
this.setData({
loginType: 'phone',
qrcodeSessionId: '',
qrcodeImage: '',
qrcodeExpired: false,
qrcodeError: '',
statusText: '',
statusDesc: '',
qrUrl: '',
qrId: '',
qrCode: '',
// 同时清空验证码登录的数据
phoneNumber: '',
verificationCode: '',
countdown: 0,
isCounting: false
});
},
// 切换到扫码登录
async switchToQRCode() {
// 清空验证码登录的数据
this.setData({
loginType: 'qrcode',
phoneNumber: '',
verificationCode: '',
countdown: 0,
isCounting: false,
qrcodeLoading: true,
loadingText: '正在加载二维码...',
qrcodeError: '' // 清空错误提示
});
try {
// 调用Go后端接口启动扫码登录
const requestUrl = `${API.baseURL}/api/xhs/qrcode/start`;
console.log('[扫码登录] 请求URL:', requestUrl);
console.log('[扫码登录] 请求时间:', new Date().toISOString());
console.log('[扫码登录] Token:', wx.getStorageSync('token') ? '已设置' : '未设置');
const data: any = await new Promise((resolve, reject) => {
wx.request({
url: requestUrl,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
success: (res) => {
console.log('[扫码登录] 响应状态:', res.statusCode);
console.log('[扫码登录] 响应数据:', res.data);
resolve(res.data);
},
fail: (err) => {
console.error('[扫码登录] 请求失败:', err);
reject(err);
}
});
});
if (data.code === 0 && data.data) {
console.log('二维码数据:', data.data);
this.setData({
qrcodeLoading: false,
qrcodeSessionId: data.data.session_id,
qrcodeImage: data.data.qrcode_image,
statusText: data.data.status_text || '请使用小红书APP扫码',
statusDesc: data.data.status_desc || '',
qrcodeExpired: data.data.is_expired || false,
// 保存二维码创建信息
qrUrl: data.data.qr_url || '',
qrId: data.data.qr_id || '',
qrCode: data.data.qr_code || ''
});
// 如果有二维码URL,打印出来
if (data.data.qr_url) {
console.log('='.repeat(50));
console.log('二维码登录链接:');
console.log(data.data.qr_url);
console.log('二维码ID:', data.data.qr_id);
console.log('二维码code:', data.data.qr_code);
console.log('='.repeat(50));
}
// 开始轮询二维码状态
this.startQRCodePolling();
} else {
throw new Error(data.message || '获取二维码失败');
}
} catch (error: any) {
// 显示错误提示在二维码区域,不跳转到手机号登录
this.setData({
qrcodeLoading: false,
qrcodeError: error.message || '加载失败,请重试'
});
}
},
// 开始轮询二维码状态
startQRCodePolling() {
if (this.data.qrcodePolling) {
return; // 已经在轮询了
}
this.setData({
qrcodePolling: true
});
const pollTimer = setInterval(() => {
this.checkQRCodeStatus();
}, 2000); // 每2秒轮询一次
this.setData({
pollTimer: pollTimer
});
},
// 停止轮询二维码状态
stopQRCodePolling() {
if (this.data.pollTimer) {
clearInterval(this.data.pollTimer);
this.setData({
pollTimer: null,
qrcodePolling: false
});
}
},
// 检查二维码状态
async checkQRCodeStatus() {
const { qrcodeSessionId } = this.data;
if (!qrcodeSessionId) {
return;
}
try {
const requestUrl = `${API.baseURL}/api/xhs/qrcode/status`;
console.log('[扫码状态] 请求URL:', requestUrl);
console.log('[扫码状态] SessionID:', qrcodeSessionId);
const data: any = await new Promise((resolve, reject) => {
wx.request({
url: requestUrl,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: {
session_id: qrcodeSessionId
},
success: (res) => {
console.log('[扫码状态] 响应:', res.data);
resolve(res.data);
},
fail: (err) => {
console.error('[扫码状态] 请求失败:', err);
reject(err);
}
});
});
// 检查是否session失效(code=2)
if (data.code === 2 && data.data && data.data.session_expired) {
console.log('⚠️ 会话已失效,停止轮询并显示提示');
// 停止轮询
this.stopQRCodePolling();
// 显示提示
this.setData({
qrcodeExpired: true,
statusText: '服务已重启,请刷新二维码',
statusDesc: '点击刷新'
});
return;
}
if (data.code === 0 && data.data) {
// 检查是否扫码登录成功
if (data.data.login_success) {
console.log('\u2705 \u626b\u7801\u767b\u5f55\u6210\u529f\uff01', data.data);
// 停止轮询
this.stopQRCodePolling();
// 显示加载状态
this.setData({
showLoading: true,
loadingText: '登录成功,正在保存...'
});
// 保存登录信息
await this.saveQRCodeLoginInfo(data.data);
return;
}
// 更新二维码状态
this.setData({
qrcodeImage: data.data.qrcode_image,
statusText: data.data.status_text || '',
statusDesc: data.data.status_desc || '',
qrcodeExpired: data.data.is_expired || false
});
// 如果二维码过期,停止轮询
if (data.data.is_expired) {
this.stopQRCodePolling();
this.setData({
statusDesc: '点击刷新'
});
}
}
} catch (error: any) {
console.error('检查二维码状态失败:', error);
}
},
// 刷新二维码
async refreshQRCode() {
const { qrcodeSessionId } = this.data;
if (!qrcodeSessionId) {
return;
}
this.setData({
showLoading: true,
loadingText: '正在刷新二维码...'
});
try {
const requestUrl = `${API.baseURL}/api/xhs/qrcode/refresh`;
console.log('[刷新二维码] 请求URL:', requestUrl);
console.log('[刷新二维码] SessionID:', qrcodeSessionId);
const data: any = await new Promise((resolve, reject) => {
wx.request({
url: requestUrl,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: {
session_id: qrcodeSessionId
},
success: (res) => {
console.log('[刷新二维码] 响应:', res.data);
resolve(res.data);
},
fail: (err) => {
console.error('[刷新二维码] 请求失败:', err);
reject(err);
}
});
});
if (data.code === 0 && data.data) {
console.log('刷新二维码数据:', data.data);
this.setData({
showLoading: false,
qrcodeImage: data.data.qrcode_image,
statusText: data.data.status_text || '请使用小红书APP扫码',
statusDesc: data.data.status_desc || '',
qrcodeExpired: data.data.is_expired || false,
// 更新二维码创建信息
qrUrl: data.data.qr_url || '',
qrId: data.data.qr_id || '',
qrCode: data.data.qr_code || ''
});
// 如果有二维码URL,打印出来
if (data.data.qr_url) {
console.log('='.repeat(50));
console.log('刷新后的二维码登录链接:');
console.log(data.data.qr_url);
console.log('二维码ID:', data.data.qr_id);
console.log('二维码code:', data.data.qr_code);
console.log('='.repeat(50));
}
// 重新开始轮询
this.stopQRCodePolling();
this.startQRCodePolling();
} else if (data.code === 3 && data.data && data.data.need_restart) {
// 需要重启扫码登录
console.log('⚠️ 页面已失效,重新启动扫码登录');
this.setData({
showLoading: false
});
wx.showToast({
title: '页面已失效,正在重新启动...',
icon: 'none',
duration: 2000
});
// 停止轮询
this.stopQRCodePolling();
// 等待提示显示后重新启动
setTimeout(() => {
this.switchToQRCode();
}, 1000);
} else {
throw new Error(data.message || '刷新失败');
}
} catch (error: any) {
this.setData({
showLoading: false
});
wx.showToast({
title: error.message || '刷新失败,请重试',
icon: 'none',
duration: 2000
});
}
},
// 复制二维码链接
copyQRUrl() {
const { qrUrl } = this.data;
if (!qrUrl) {
wx.showToast({
title: '暂无链接',
icon: 'none',
duration: 2000
});
return;
}
wx.setClipboardData({
data: qrUrl,
success: () => {
wx.showToast({
title: '链接已复制',
icon: 'success',
duration: 2000
});
},
fail: () => {
wx.showToast({
title: '复制失败',
icon: 'none',
duration: 2000
});
}
});
},
// 取消扫码登录,释放浏览器资源
cancelQRCodeLogin(sessionId: string) {
if (!sessionId) {
return;
}
wx.request({
url: `${API.baseURL}/api/xhs/qrcode/cancel`,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: { session_id: sessionId },
success: () => {
console.log('[扫码登录] 已释放浏览器资源');
},
fail: (err) => {
console.error('[扫码登录] 释放失败:', err);
}
});
},
// 保存扫码登录信息
async saveQRCodeLoginInfo(loginData: any) {
try {
console.log('开始保存扫码登录信息...', loginData);
// 获取用户token
const token = wx.getStorageSync('token');
if (!token) {
throw new Error('未登录,请先登录');
}
// 扫码登录和验证码登录不一样需要调用Python后端保存
// Python后端已经返回了完整的登录数据需要调用Go后端保存到数据库
// 但是Go后端的 /api/employee/bind-xhs 接口只支持验证码登录
// 所以我们直接调用Python后端的 /api/xhs/save-bind-info 接口
const saveResult: any = await new Promise((resolve, reject) => {
wx.request({
url: `${API.baseURL}/api/xhs/save-qrcode-login`,
method: 'POST',
header: {
'Content-Type': 'application/json'
},
data: {
employee_id: (wx.getStorageSync('employeeInfo') || {}).id,
cookies_full: loginData.cookies_full || [],
user_info: loginData.user_info || {},
login_state: loginData.login_state || {}
},
success: (res) => resolve(res.data),
fail: (err) => reject(err)
});
});
console.log('保存结果:', saveResult);
// Go后端返回 code=200, Python后端返回 code=0
if (saveResult.code === 0 || saveResult.code === 200) {
// 保存成功,释放浏览器资源
this.cancelQRCodeLogin(this.data.qrcodeSessionId);
// 更新本地缓存
const bindings = wx.getStorageSync('socialBindings') || {};
const userInfo = loginData.user_info || {};
bindings.xiaohongshu = {
phone: userInfo.red_id || '',
xhs_account: userInfo.nickname || '小红书用户',
bindTime: new Date().getTime(),
cookieExpired: false
};
wx.setStorageSync('socialBindings', bindings);
console.log('[扫码登录] 绑定成功,已更新本地缓存', bindings.xiaohongshu);
// 显示成功提示
this.setData({
showLoading: false,
showSuccess: true
});
// 2秒后跳转回个人中心
setTimeout(() => {
this.setData({ showSuccess: false });
wx.navigateBack();
}, 2000);
} else {
throw new Error(saveResult.message || '保存失败');
}
} catch (error: any) {
console.error('保存扫码登录信息失败:', error);
this.setData({
showLoading: false
});
wx.showToast({
title: error.message || '绑定失败,请重试',
icon: 'none',
duration: 2000
});
// 停止轮询并切换回手机号登录
this.stopQRCodePolling();
this.switchToPhone();
}
}
});

View File

@@ -12,46 +12,104 @@
<text class="page-title">请绑定小红书账号</text>
<text class="page-subtitle">手机号未注册小红书会导致绑定失败</text>
<!-- 二维码验证区域 -->
<view class="qrcode-section" wx:if="{{needCaptcha && qrcodeImage}}">
<text class="qrcode-title">请使用小红书APP扫描二维码</text>
<image class="qrcode" src="{{qrcodeImage}}" mode="aspectFit"></image>
<text class="qrcode-hint">扫码后即可继续绑定流程</text>
<!-- 登录方式切换 -->
<view class="login-type-tabs">
<view class="tab {{loginType === 'phone' ? 'active' : ''}}" bindtap="switchToPhone">
<text>手机号登录</text>
</view>
<view class="tab {{loginType === 'qrcode' ? 'active' : ''}}" bindtap="switchToQRCode">
<text>扫码登录</text>
</view>
</view>
<view class="bind-form" wx:if="{{!needCaptcha}}">
<view class="input-row">
<text class="label">手机号</text>
<picker mode="selector" range="{{countryCodes}}" value="{{countryCodeIndex}}" bindchange="onCountryCodeChange">
<view class="prefix">{{countryCodes[countryCodeIndex]}}</view>
</picker>
<input
type="number"
placeholder="输入手机号"
maxlength="11"
bindinput="onPhoneInput"
value="{{phone}}"
/>
<!-- 二维码扫码登录区域 -->
<view class="qrcode-login-section" wx:if="{{loginType === 'qrcode'}}">
<view class="qrcode-container">
<!-- 加载中 -->
<view class="qrcode-loading" wx:if="{{qrcodeLoading}}">
<view class="loading-spinner"></view>
<text class="loading-text">{{loadingText}}</text>
</view>
<!-- 二维码图片 -->
<image class="qrcode-img" src="{{qrcodeImage}}" mode="aspectFit" wx:if="{{qrcodeImage && !qrcodeLoading}}"></image>
<!-- 二维码状态覆盖层(只在过期或出错时显示) -->
<view class="qrcode-status" wx:if="{{qrcodeExpired && !qrcodeLoading}}">
<view class="status-content">
<text class="status-text">{{statusText || '二维码已过期'}}</text>
<text class="status-desc" bindtap="refreshQRCode">点击刷新</text>
</view>
</view>
<!-- 错误提示 -->
<view class="qrcode-error" wx:if="{{qrcodeError && !qrcodeLoading}}">
<view class="error-content">
<text class="error-icon">⚠️</text>
<text class="error-text">{{qrcodeError}}</text>
<text class="error-retry" bindtap="switchToQRCode">点击重试</text>
</view>
</view>
</view>
<!-- 登录链接显示区域 -->
<view class="qr-url-section" wx:if="{{qrUrl}}">
<view class="url-label">登录链接可复制到浏览器或小红书APP</view>
<view class="url-content">
<text class="url-text">{{qrUrl}}</text>
</view>
<button class="copy-btn" bindtap="copyQRUrl">复制链接</button>
</view>
<view class="qrcode-tips">
<text class="tip-text">请使用小红书APP扫描二维码</text>
<text class="tip-desc">扫码后即可完成绑定</text>
</view>
</view>
<!-- 手机号登录区域 -->
<view class="phone-login-section" wx:if="{{loginType === 'phone'}}">
<!-- 二维码验证区域(验证码验证时出现) -->
<view class="qrcode-section" wx:if="{{needCaptcha && qrcodeImage}}">
<text class="qrcode-title">请使用小红书APP扫描二维码</text>
<image class="qrcode" src="{{qrcodeImage}}" mode="aspectFit"></image>
<text class="qrcode-hint">扫码后即可继续绑定流程</text>
</view>
<view class="input-row">
<text class="label">验证码</text>
<input
type="number"
placeholder="请输入验证码"
maxlength="6"
bindinput="onCodeInput"
value="{{code}}"
/>
<button
class="get-code {{countdown > 0 ? 'disabled' : ''}}"
bindtap="getVerifyCode"
>
{{codeButtonText}}
</button>
</view>
<view class="bind-form" wx:if="{{!needCaptcha}}">
<view class="input-row">
<text class="label">手机号</text>
<picker mode="selector" range="{{countryCodes}}" value="{{countryCodeIndex}}" bindchange="onCountryCodeChange">
<view class="prefix">{{countryCodes[countryCodeIndex]}}</view>
</picker>
<input
type="number"
placeholder="输入手机号"
maxlength="11"
bindinput="onPhoneInput"
value="{{phone}}"
/>
</view>
<button class="red-btn" bindtap="bindAccount">验证并绑定</button>
<view class="input-row">
<text class="label">验证码</text>
<input
type="number"
placeholder="请输入验证码"
maxlength="6"
bindinput="onCodeInput"
value="{{code}}"
/>
<button
class="get-code {{countdown > 0 ? 'disabled' : ''}}"
bindtap="getVerifyCode"
>
{{codeButtonText}}
</button>
</view>
<button class="red-btn" bindtap="bindAccount">验证并绑定</button>
</view>
</view>
</view>
@@ -69,7 +127,7 @@
</view>
</view>
<view class="toast-overlay" wx:if="{{showLoading}}">
<view class="toast-overlay" wx:if="{{showLoading && loginType === 'phone'}}">
<view class="toast">
<view class="toast-loading"></view>
<text class="toast-text">{{loadingText}}</text>

View File

@@ -52,10 +52,168 @@ page {
.page-subtitle {
font-size: 28rpx;
color: #999;
margin-bottom: 80rpx;
margin-bottom: 48rpx;
display: block;
}
/* 登录方式切换 */
.login-type-tabs {
width: 100%;
display: flex;
margin-bottom: 48rpx;
border-radius: 12rpx;
background: #F5F5F5;
padding: 8rpx;
}
.login-type-tabs .tab {
flex: 1;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
font-size: 30rpx;
color: #666;
transition: all 0.3s;
}
.login-type-tabs .tab.active {
background: #fff;
color: #FF2442;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
/* 二维码扫码登录区域 */
.qrcode-login-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.qrcode-container {
width: 500rpx;
height: 500rpx;
position: relative;
margin-bottom: 48rpx;
}
.qrcode-img {
width: 100%;
height: 100%;
border: 2rpx solid #E5E5E5;
border-radius: 16rpx;
background: #fff;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
/* 二维码状态覆盖层 */
.qrcode-status {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.status-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.status-text {
font-size: 32rpx;
font-weight: 500;
color: #333;
text-align: center;
}
.status-desc {
font-size: 28rpx;
color: #FF2442;
text-decoration: underline;
cursor: pointer;
}
.qrcode-tips {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.tip-text {
font-size: 32rpx;
font-weight: 500;
color: #333;
text-align: center;
}
.tip-desc {
font-size: 26rpx;
color: #999;
text-align: center;
}
/* 二维码URL显示区域 */
.qr-url-section {
width: 100%;
margin-bottom: 32rpx;
padding: 24rpx;
background: #F8F8F8;
border-radius: 12rpx;
}
.url-label {
font-size: 26rpx;
color: #666;
margin-bottom: 16rpx;
text-align: center;
}
.url-content {
width: 100%;
padding: 20rpx;
background: #fff;
border-radius: 8rpx;
margin-bottom: 16rpx;
word-break: break-all;
}
.url-text {
font-size: 24rpx;
color: #333;
line-height: 1.6;
}
.copy-btn {
width: 100%;
height: 72rpx;
background: linear-gradient(135deg, #FF2442 0%, #FF4F6A 100%);
border-radius: 8rpx;
font-size: 30rpx;
font-weight: 500;
color: #fff;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
/* 手机号登录区域 */
.phone-login-section {
width: 100%;
}
/* 二维码验证区域 */
.qrcode-section {
width: 100%;
@@ -274,3 +432,77 @@ page {
transform: rotate(360deg);
}
}
/* 二维码加载中 */
.qrcode-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24rpx;
border: 2rpx solid #E5E5E5;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
.loading-spinner {
width: 64rpx;
height: 64rpx;
border: 4rpx solid #E5E5E5;
border-top-color: #FF2442;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-text {
font-size: 28rpx;
color: #666;
}
/* 二维码错误提示 */
.qrcode-error {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #FFE5E5;
box-shadow: 0 8rpx 24rpx rgba(255, 36, 66, 0.1);
}
.error-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
padding: 48rpx;
}
.error-icon {
font-size: 80rpx;
}
.error-text {
font-size: 28rpx;
color: #666;
text-align: center;
line-height: 1.6;
}
.error-retry {
font-size: 28rpx;
color: #FF2442;
text-decoration: underline;
margin-top: 16rpx;
}

View File

@@ -1,8 +0,0 @@
{
"navigationBarTitleText": "已发布文章",
"navigationBarBackgroundColor": "#07c160",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5",
"backgroundTextStyle": "light"
}

View File

@@ -1,219 +0,0 @@
// pages/profile/published/published.ts
import { EmployeeService, PublishRecord as ApiPublishRecord } from '../../../services/employee';
import { getImageUrl } from '../../../utils/util';
interface Article {
id: number;
productName: string;
title: string;
content: string;
tags: string[];
images: Array<{
id: number;
image_url: string;
image_thumb_url: string;
sort_order: number;
keywords_name: string;
}>;
createTime: string;
status: string;
}
Page({
data: {
articles: [] as Article[],
hasLoaded: false // 是否已加载过数据
},
onLoad() {
this.loadArticles();
},
onShow() {
// 仅在未加载过数据时加载
if (!this.data.hasLoaded) {
this.loadArticles();
}
},
// 下拉刷新
onPullDownRefresh() {
this.loadArticles(true);
},
// 初始化模拟数据
initMockData() {
const existingArticles = wx.getStorageSync('myArticles') || [];
// 如果已经有数据,不重复初始化
if (existingArticles.length > 0) return;
const mockArticles = [
{
id: Date.now() + 1,
productName: '兰蔻小黑瓶精华液',
title: '【种草分享】兰蔻小黑瓶精华液使用体验💕',
content: '姐妹们!今天必须来跟大家分享一下我最近入手的宝藏好物——兰蔻小黑瓶精华液!\n\n✨使用感受\n用了一段时间真的太爱了质感超级好完全超出我的预期。包装也非常精致送人自用都很合适。\n\n🌟推荐理由\n1. 品质优秀,性价比超高\n2. 使用体验一级棒\n3. 颜值在线,拿出来超有面子',
tags: ['种草分享', '好物推荐', '必买清单', '真实测评'],
createTime: new Date(Date.now() - 3600000 * 2).toISOString(), // 2小时前
status: 'published'
},
{
id: Date.now() + 2,
productName: 'SK-II神仙水',
title: '真香警告SK-II神仙水实测分享',
content: '集美们看过来今天给大家带来SK-II神仙水的真实使用感受~\n\n🎯第一印象\n收到货的那一刻就被惊艳到了包装精美细节满满完全是高端货的质感。\n\n💫使用体验\n用了几天下来真的是越用越喜欢质量很好用起来特别顺手完全就是我想要的样子',
tags: ['真实测评', '使用心得', '好物安利', '值得入手'],
createTime: new Date(Date.now() - 86400000).toISOString(), // 1天前
status: 'published'
},
{
id: Date.now() + 3,
productName: 'AirPods Pro 2',
title: '【数码种草】AirPods Pro 2使用一周真实感受',
content: '作为一个数码发烧友终于入手了AirPods Pro 2用了一周来给大家分享一下真实体验。\n\n🎵音质表现\n音质真的提升很明显特别是低音部分听流行音乐和播客都非常棒\n\n🔇降噪效果\n降噪功能强大地铁里基本听不到外界噪音专注力提升100%',
tags: ['数码好物', '音质体验', '降噪耳机'],
createTime: new Date(Date.now() - 172800000).toISOString(), // 2天前
status: 'published'
},
{
id: Date.now() + 4,
productName: 'Dior烈艳蓝金口红',
title: '【美妆必入】Dior烈艳蓝金口红999试色分享',
content: '今天来分享我最近的心头好Dior烈艳蓝金口红999号色真的太美了\n\n💄色号分析\n999是经典的正红色白皮黄皮都能驾驭显白效果一级棒\n\n✨质地感受\n润而不油持久度也很好吃饭喝水后还有淡淡的颜色留存。',
tags: ['美妆种草', '口红推荐', 'Dior蓝金'],
createTime: new Date(Date.now() - 259200000).toISOString(), // 3天前
status: 'published'
},
{
id: Date.now() + 5,
productName: '雅诗兰黛小棕瓶',
content: '雅诗兰黛小棕瓶是我用过最好的精华!修复力强,用了一瓶皮肤状态明显提升。\n\n🌟产品亮点\n含有高浓度修复精华抗氧化效果很棒长期使用能看到细纹改善。\n\n💡使用建议\n建议晚上使用配合按摩手法效果更好第二天起来皮肤非常透亮',
title: '【护肤心得】雅诗兰黛小棕瓶修复精华使用报告',
tags: ['护肤精华', '抗衰老', '修复力'],
createTime: new Date(Date.now() - 432000000).toISOString(), // 5天前
status: 'published'
}
];
wx.setStorageSync('myArticles', mockArticles);
},
// 加载文章列表
async loadArticles(isPullRefresh: boolean = false) {
try {
// 从后端API获取发布记录
const response = await EmployeeService.getMyPublishRecords(1, 50);
console.log('===== 已发布文章列表响应 =====');
console.log('response:', response);
if (response.code === 200 && response.data) {
const apiArticles = response.data.list.map((record: ApiPublishRecord) => {
console.log('处理记录:', record);
console.log('images:', record.images);
console.log('tags:', record.tags);
return {
id: record.id,
productName: record.product_name || '未知商品',
title: record.title,
content: '', // 列表页不需要显示完整内容
tags: record.tags || [],
images: (record.images || []).map((img: any) => ({
...img,
image_url: getImageUrl(img.image_url),
image_thumb_url: getImageUrl(img.image_thumb_url || img.image_url)
})),
createTime: record.publish_time,
status: 'published'
};
});
console.log('转换后的文章列表:', apiArticles);
this.setData({
articles: apiArticles,
hasLoaded: true // 标记已加载
});
console.log('setData后的articles:', this.data.articles);
if (isPullRefresh) {
wx.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
}
return;
}
} catch (error) {
console.error('加载发布记录失败:', error);
} finally {
if (isPullRefresh) {
wx.stopPullDownRefresh();
}
}
// 如果API失败尝试使用本地数据
const articles = wx.getStorageSync('myArticles') || [];
// 格式化时间
const formattedArticles = articles.map((item: Article) => {
return {
...item,
createTime: this.formatTime(item.createTime)
};
});
this.setData({
articles: formattedArticles,
hasLoaded: true
});
if (isPullRefresh) {
wx.stopPullDownRefresh();
}
},
// 格式化时间
formatTime(isoString: string): string {
const date = new Date(isoString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
},
// 查看文章详情
viewArticle(e: any) {
const index = e.currentTarget.dataset.index;
const article = this.data.articles[index];
// 跳转到文章详情页
wx.navigateTo({
url: `/pages/profile/article-detail/article-detail?id=${article.id}`
});
},
// 分享功能
onShareAppMessage() {
return {
title: '我的发布记录 - 万花筒AI助手',
path: '/pages/home/home',
imageUrl: ''
};
},
// 分享到朋友圈
onShareTimeline() {
return {
title: '万花筒AI助手 - 智能生成种草文案',
imageUrl: ''
};
}
});

View File

@@ -1,56 +0,0 @@
<!--pages/profile/published/published.wxml-->
<view class="page-container">
<!-- 空状态 -->
<view class="empty-state" wx:if="{{articles.length === 0}}">
<view class="empty-icon">📝</view>
<text class="empty-text">还没有发布任何文章</text>
<text class="empty-hint">去首页选择商品发布种草文章吧~</text>
</view>
<!-- 文章列表 -->
<scroll-view class="article-list" scroll-y wx:else>
<view
class="article-item"
wx:for="{{articles}}"
wx:key="id"
bindtap="viewArticle"
data-index="{{index}}"
>
<view class="article-content">
<view class="article-header">
<text class="article-title">{{item.title}}</text>
<text class="article-time">{{item.createTime}}</text>
</view>
<!-- 文章图片 -->
<view class="article-images" wx:if="{{item.images && item.images.length > 0}}">
<image
class="article-image"
wx:for="{{item.images}}"
wx:key="id"
wx:for-item="image"
src="{{image.image_thumb_url}}"
mode="aspectFill"
/>
</view>
<view class="article-footer">
<view class="article-product">
<text class="product-label">商品:</text>
<text class="product-name">{{item.productName}}</text>
</view>
<view class="article-tags" wx:if="{{item.tags && item.tags.length > 0}}">
<text
class="tag-item"
wx:for="{{item.tags}}"
wx:key="index"
wx:for-item="tag"
>
#{{tag}}
</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>

View File

@@ -1,146 +0,0 @@
/* pages/profile/published/published.wxss */
page {
background: #f8f8f8;
height: 100%;
}
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 60rpx;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
opacity: 0.3;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-bottom: 16rpx;
}
.empty-hint {
font-size: 26rpx;
color: #ccc;
}
/* 文章列表 */
.article-list {
flex: 1;
padding: 20rpx;
box-sizing: border-box;
overflow-y: auto;
}
.article-item {
background: white;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.article-content {
padding: 30rpx;
}
.article-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
}
.article-title {
flex: 1;
font-size: 32rpx;
color: #1a1a1a;
font-weight: bold;
line-height: 1.4;
margin-right: 20rpx;
}
.article-time {
font-size: 22rpx;
color: #999;
white-space: nowrap;
}
/* 文章图片 */
.article-images {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
margin: 20rpx 0;
}
.article-image {
width: 100%;
height: 200rpx;
border-radius: 8rpx;
background: #f5f5f5;
}
.article-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
margin-bottom: 20rpx;
}
.article-footer {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.article-product {
display: flex;
align-items: center;
}
.product-label {
font-size: 24rpx;
color: #999;
}
.product-name {
font-size: 24rpx;
color: #ff2442;
font-weight: 500;
}
.article-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
align-items: center;
}
.tag-item {
padding: 4rpx 16rpx;
background: linear-gradient(135deg, #fff5f7 0%, #ffe8ec 100%);
border-radius: 16rpx;
font-size: 22rpx;
color: #ff2442;
line-height: 1.5;
box-sizing: border-box;
}

View File

@@ -1,5 +0,0 @@
{
"navigationBarTitleText": "社交账号绑定",
"navigationBarBackgroundColor": "#07c160",
"navigationBarTextStyle": "white"
}

View File

@@ -1,188 +0,0 @@
import { EmployeeService } from '../../../services/employee';
Page({
data: {
// 小红书
xiaohongshuBinded: false,
xiaohongshuPhone: '',
xiaohongshuAccount: '',
xiaohongshuCookieExpired: false, // Cookie是否失效
xiaohongshuStatusText: '去绑定',
xiaohongshuStatusClass: '',
// 微博
weiboBinded: false,
weiboPhone: '',
// 抖音
douyinBinded: false,
douyinPhone: ''
},
onLoad() {
this.loadBindingStatus();
},
onShow() {
// 每次显示时重新加载绑定状态
this.loadBindingStatus();
// 同时从后端加载
this.loadUserProfile();
},
// 从BACKEND加载用户信息包含小红书绑定状态
async loadUserProfile() {
try {
const response = await EmployeeService.getProfile();
if (response.code === 200 && response.data) {
const userInfo = response.data;
const isBound = userInfo.is_bound_xhs === 1;
// 使用has_xhs_cookie字段判断而不是xhs_cookie
const hasCookie = userInfo.has_xhs_cookie === true;
// 判断状态
let statusText = '去绑定';
let statusClass = '';
let cookieExpired = false;
if (isBound) {
if (hasCookie) {
// 已绑定且Cookie有效
statusText = '已绑定';
statusClass = 'binded';
} else {
// 已绑定但Cookie失效
statusText = '已失效';
statusClass = 'expired';
cookieExpired = true;
}
}
this.setData({
xiaohongshuBinded: isBound,
xiaohongshuAccount: userInfo.xhs_account || '',
xiaohongshuPhone: userInfo.xhs_phone || '',
xiaohongshuCookieExpired: cookieExpired,
xiaohongshuStatusText: statusText,
xiaohongshuStatusClass: statusClass
});
// 更新本地存储
if (isBound) {
const bindings = wx.getStorageSync('socialBindings') || {};
bindings.xiaohongshu = {
phone: userInfo.xhs_phone,
xhs_account: userInfo.xhs_account,
bindTime: userInfo.bound_at || new Date().getTime(),
cookieExpired: cookieExpired
};
wx.setStorageSync('socialBindings', bindings);
}
}
} catch (error) {
console.error('加载用户信息失败:', error);
}
},
// 加载绑定状态
loadBindingStatus() {
const bindings = wx.getStorageSync('socialBindings') || {};
this.setData({
xiaohongshuBinded: !!bindings.xiaohongshu,
xiaohongshuPhone: (bindings.xiaohongshu && bindings.xiaohongshu.phone) || '',
weiboBinded: !!bindings.weibo,
weiboPhone: (bindings.weibo && bindings.weibo.phone) || '',
douyinBinded: !!bindings.douyin,
douyinPhone: (bindings.douyin && bindings.douyin.phone) || ''
});
},
// 跳转到平台绑定页面
goToPlatformBind(e: any) {
const platform = e.currentTarget.dataset.platform;
// 如果是小红书
if (platform === 'xiaohongshu') {
// Cookie失效直接跳转重新绑定
if (this.data.xiaohongshuCookieExpired) {
wx.navigateTo({
url: '/pages/profile/platform-bind/platform-bind'
});
return;
}
// 已绑定且Cookie有效显示解绑确认
if (this.data.xiaohongshuBinded && !this.data.xiaohongshuCookieExpired) {
this.handleUnbindXHS();
return;
}
// 未绑定,跳转到绑定页
wx.navigateTo({
url: '/pages/profile/platform-bind/platform-bind'
});
return;
}
wx.navigateTo({
url: `/pages/profile/platform-bind/platform-bind?platform=${platform}`
});
},
// 解绑小红书
handleUnbindXHS() {
wx.showModal({
title: '确认解绑',
content: '确定要解绑小红书账号吗?',
confirmText: '解绑',
confirmColor: '#FF6B6B',
success: async (res) => {
if (res.confirm) {
try {
console.log('开始解绑小红书...');
const response = await EmployeeService.unbindXHS();
console.log('解绑响应:', response);
if (response.code === 200) {
wx.showToast({
title: '解绑成功',
icon: 'success'
});
// 清除本地存储
const bindings = wx.getStorageSync('socialBindings') || {};
delete bindings.xiaohongshu;
wx.setStorageSync('socialBindings', bindings);
// 刷新页面
this.setData({
xiaohongshuBinded: false,
xiaohongshuPhone: '',
xiaohongshuAccount: ''
});
// 重新加载用户信息
this.loadUserProfile();
} else {
console.error('解绑失败:', response.message);
wx.showToast({
title: response.message || '解绑失败',
icon: 'none'
});
}
} catch (error) {
console.error('解绑失败:', error);
wx.showToast({
title: '解绑失败',
icon: 'none'
});
}
}
}
});
}
});

View File

@@ -1,51 +0,0 @@
<!--pages/profile/social-binding/social-binding.wxml-->
<view class="page-container">
<scroll-view class="content-scroll" scroll-y>
<!-- 小红书 -->
<view class="platform-item" bindtap="goToPlatformBind" data-platform="xiaohongshu">
<view class="platform-left">
<view class="platform-icon xiaohongshu"></view>
<view class="platform-info">
<text class="platform-name">小红书</text>
<text class="platform-desc">{{xiaohongshuBinded ? xiaohongshuPhone : '未绑定'}}</text>
</view>
</view>
<view class="platform-right">
<view class="bind-status {{xiaohongshuStatusClass}}">{{xiaohongshuStatusText}}</view>
<text class="item-arrow"></text>
</view>
</view>
<!-- 微博 -->
<view class="platform-item" bindtap="goToPlatformBind" data-platform="weibo">
<view class="platform-left">
<view class="platform-icon weibo"></view>
<view class="platform-info">
<text class="platform-name">微博</text>
<text class="platform-desc">{{weiboBinded ? weiboPhone : '未绑定'}}</text>
</view>
</view>
<view class="platform-right">
<view class="bind-status {{weiboBinded ? 'binded' : ''}}">{{weiboBinded ? '已绑定' : '去绑定'}}</view>
<text class="item-arrow"></text>
</view>
</view>
<!-- 抖音 -->
<view class="platform-item" bindtap="goToPlatformBind" data-platform="douyin">
<view class="platform-left">
<view class="platform-icon douyin"></view>
<view class="platform-info">
<text class="platform-name">抖音</text>
<text class="platform-desc">{{douyinBinded ? douyinPhone : '未绑定'}}</text>
</view>
</view>
<view class="platform-right">
<view class="bind-status {{douyinBinded ? 'binded' : ''}}">{{douyinBinded ? '已绑定' : '去绑定'}}</view>
<text class="item-arrow"></text>
</view>
</view>
</scroll-view>
</view>

View File

@@ -1,112 +0,0 @@
/* pages/profile/social-binding/social-binding.wxss */
page {
background: #f8f8f8;
height: 100%;
}
.page-container {
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.content-scroll {
flex: 1;
width: 100%;
padding: 20rpx;
box-sizing: border-box;
}
/* 平台列表项 */
.platform-item {
background: white;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
}
.platform-item:active {
transform: scale(0.98);
background: #fafafa;
}
.platform-left {
display: flex;
align-items: center;
gap: 20rpx;
flex: 1;
}
.platform-icon {
width: 88rpx;
height: 88rpx;
border-radius: 16rpx;
flex-shrink: 0;
}
.platform-icon.xiaohongshu {
background: #07c160;
}
.platform-icon.weibo {
background: linear-gradient(135deg, #ff8200 0%, #ffb84d 100%);
}
.platform-icon.douyin {
background: linear-gradient(135deg, #000000 0%, #333333 100%);
}
.platform-info {
display: flex;
flex-direction: column;
gap: 8rpx;
flex: 1;
}
.platform-name {
font-size: 32rpx;
font-weight: 600;
color: #1a1a1a;
}
.platform-desc {
font-size: 24rpx;
color: #999;
}
.platform-right {
display: flex;
align-items: center;
gap: 12rpx;
}
.bind-status {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
background: #f5f5f5;
color: #999;
}
.bind-status.binded {
background: #e8f5e9;
color: #4caf50;
}
.bind-status.expired {
background: #fff3e0;
color: #ff9800;
}
.item-arrow {
font-size: 40rpx;
color: #d0d0d0;
font-weight: 300;
}

View File

@@ -1,5 +0,0 @@
{
"navigationBarTitleText": "个人资料",
"navigationBarBackgroundColor": "#07c160",
"navigationBarTextStyle": "white"
}

View File

@@ -1,171 +0,0 @@
// pages/profile/user-info/user-info.ts
import { EmployeeService } from '../../../services/employee';
Page({
data: {
username: '',
nickname: '',
enterpriseName: '',
avatar: '',
phone: '',
email: '',
role: '',
roleText: '',
saving: false
},
async onLoad() {
await this.loadUserInfo();
},
async loadUserInfo() {
try {
const response = await EmployeeService.getProfile();
if (response.code === 200 && response.data) {
const userInfo: any = response.data;
const role = userInfo.role || '';
const nickname = userInfo.nickname || userInfo.name || userInfo.username || '';
this.setData({
username: userInfo.name || '',
nickname,
enterpriseName: userInfo.enterprise_name || '',
avatar: userInfo.avatar || '',
phone: userInfo.phone || '',
email: userInfo.email || '',
role,
roleText: this.translateRole(role)
});
}
} catch (error) {
console.error('加载个人信息失败:', error);
}
},
translateRole(role: string): string {
switch (role) {
case 'admin':
return '管理员';
case 'reviewer':
return '审核员';
case 'publisher':
return '发布员';
default:
return '内容管理员';
}
},
onNicknameInput(e: any) {
this.setData({ nickname: e.detail.value });
},
onEmailInput(e: any) {
this.setData({ email: e.detail.value });
},
async onChooseWechatAvatar(e: any) {
if (this.data.saving) return;
const avatarUrl = e.detail.avatarUrl;
if (!avatarUrl) {
return;
}
try {
wx.showLoading({ title: '上传中...', mask: true });
const uploadRes = await EmployeeService.uploadImage(avatarUrl);
wx.hideLoading();
if (uploadRes.code === 200 && uploadRes.data) {
this.setData({ avatar: uploadRes.data.image_url });
} else {
wx.showToast({ title: uploadRes.message || '上传失败', icon: 'none' });
}
} catch (error: any) {
wx.hideLoading();
if (error && error.errMsg && error.errMsg.indexOf('cancel') !== -1) {
return;
}
console.error('选择头像失败:', error);
wx.showToast({ title: '选择头像失败', icon: 'none' });
}
},
onUseWechatNickname() {
if (this.data.saving) return;
wx.getUserProfile({
desc: '用于完善个人资料',
success: (res) => {
const nickname = (res.userInfo && res.userInfo.nickName) || '';
if (!nickname) {
wx.showToast({ title: '未获取到微信昵称', icon: 'none' });
return;
}
this.setData({
nickname
});
},
fail: (error) => {
if (error && error.errMsg && error.errMsg.indexOf('auth deny') !== -1) {
wx.showToast({ title: '未授权微信昵称', icon: 'none' });
} else {
wx.showToast({ title: '获取微信昵称失败', icon: 'none' });
}
}
});
},
onFormSubmit(e: any) {
const { nickname, email } = e.detail.value || {};
this.setData({
nickname,
email
}, () => {
this.handleSave();
});
},
async handleSave() {
if (this.data.saving) return;
const { nickname, email, avatar } = this.data;
if (!nickname || nickname.trim().length === 0) {
wx.showToast({ title: '昵称不能为空', icon: 'none' });
return;
}
if (email && email.indexOf('@') === -1) {
wx.showToast({ title: '邮箱格式不正确', icon: 'none' });
return;
}
this.setData({ saving: true });
wx.showLoading({ title: '保存中...', mask: true });
try {
const res = await EmployeeService.updateProfile({
nickname: nickname.trim(),
email: email ? email.trim() : undefined,
avatar
});
wx.hideLoading();
if (res.code === 200) {
wx.showToast({ title: '保存成功', icon: 'success' });
} else {
wx.showToast({ title: res.message || '保存失败', icon: 'none' });
}
} catch (error) {
wx.hideLoading();
console.error('保存个人资料失败:', error);
wx.showToast({ title: '保存失败', icon: 'none' });
} finally {
this.setData({ saving: false });
}
}
});

View File

@@ -1,57 +0,0 @@
<!--pages/profile/user-info/user-info.wxml-->
<view class="page-container">
<form bindsubmit="onFormSubmit">
<!-- 资料编辑区域 -->
<view class="info-section">
<!-- 头像,使用微信官方选择头像能力 -->
<view class="info-item">
<view class="item-label">头像</view>
<view class="item-value">
<button class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseWechatAvatar">
<view class="avatar-preview">
<image wx:if="{{avatar}}" class="avatar-img-small" src="{{avatar}}" mode="aspectFill" />
<text wx:else class="avatar-icon"></text>
</view>
</button>
<text class="item-arrow"></text>
</view>
</view>
<!-- 昵称,可编辑 -->
<view class="info-item">
<view class="item-label">昵称</view>
<view class="item-value">
<input class="value-input" name="nickname" type="nickname" value="{{nickname}}" placeholder="请输入昵称" maxlength="20" bindinput="onNicknameInput" />
</view>
</view>
<!-- 邮箱,可编辑 -->
<view class="info-item">
<view class="item-label">邮箱</view>
<view class="item-value">
<input class="value-input" name="email" value="{{email}}" placeholder="请输入邮箱" bindinput="onEmailInput" />
</view>
</view>
<!-- 手机号,只读 -->
<view class="info-item">
<view class="item-label">手机号</view>
<view class="item-value">
<text class="value-text">{{phone || '未绑定'}}</text>
</view>
</view>
<!-- 角色,只读 -->
<view class="info-item">
<view class="item-label">角色</view>
<view class="item-value">
<text class="value-text role">{{roleText}}</text>
</view>
</view>
</view>
<view class="save-section">
<button class="save-btn" loading="{{saving}}" disabled="{{saving}}" form-type="submit">保存修改</button>
</view>
</form>
</view>

View File

@@ -1,190 +0,0 @@
/* pages/profile/user-info/user-info.wxss */
page {
background: #f8f8f8;
}
.page-container {
min-height: 100vh;
padding-bottom: 120rpx;
}
/* 头部区域,统一为绿色背景,与个人中心一致 */
.header-section {
width: 100%;
background: #07c160;
padding: 50rpx 30rpx 70rpx;
box-sizing: border-box;
}
.user-card {
display: flex;
align-items: center;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: #ffffff;
border: 4rpx solid rgba(255, 255, 255, 0.3);
margin-right: 24rpx;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 60rpx;
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
}
.username {
font-size: 36rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 8rpx;
}
.enterprise-name {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
}
.info-section {
width: 100%;
background: #ffffff;
margin-top: 20rpx;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.item-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.item-value {
display: flex;
align-items: center;
justify-content: flex-end;
flex: 1;
gap: 0;
}
.value-text {
font-size: 28rpx;
color: #666;
}
.value-text.role {
color: #07c160;
font-weight: 600;
}
.value-input {
min-width: 360rpx;
text-align: right;
font-size: 28rpx;
color: #333;
}
.item-arrow {
font-size: 36rpx;
color: #ccc;
}
.avatar-preview {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-img-small {
width: 100%;
height: 100%;
border-radius: 50%;
}
.avatar-btn {
padding: 0;
margin: 0;
border: none;
background: transparent;
}
.avatar-btn::after {
border: none;
}
.avatar-icon {
width: 50rpx;
height: 50rpx;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23ffffff"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.wechat-sync-btn {
padding: 12rpx 24rpx;
font-size: 24rpx;
color: #07c160;
border-radius: 999rpx;
border: 1rpx solid #07c160;
background-color: #ffffff;
}
.wechat-sync-btn::after {
border: none;
}
.save-section {
width: 100%;
padding: 40rpx 30rpx;
}
.save-btn {
width: 80%;
max-width: 600rpx;
margin: 0 auto;
background: #07c160;
color: #ffffff;
border: none;
border-radius: 999rpx;
padding: 24rpx 0;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
}
.save-btn::after {
border: none;
}
.save-btn:active {
opacity: 0.9;
transform: translateY(2rpx);
}

View File

@@ -1,5 +0,0 @@
{
"navigationBarTitleText": "小红书登录",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}

View File

@@ -1,291 +0,0 @@
// pages/profile/xhs-login/xhs-login.ts
import { EmployeeService } from '../../../services/employee';
Page({
data: {
countryCode: '+86',
phone: '',
code: '',
codeSending: false,
countdown: 0,
agreedToTerms: false,
showAgreementModal: false,
loginLoading: false // 绑定加载状态,防止重复提交
},
// 选择国家区号
selectCountryCode() {
const codes = ['+86', '+852', '+853', '+886'];
const names = ['中国大陆', '中国香港', '中国澳门', '中国台湾'];
wx.showActionSheet({
itemList: names.map((name, index) => `${name} ${codes[index]}`),
success: (res) => {
this.setData({
countryCode: codes[res.tapIndex]
});
}
});
},
// 手机号输入
onPhoneInput(e: any) {
this.setData({
phone: e.detail.value
});
},
// 验证码输入
onCodeInput(e: any) {
this.setData({
code: e.detail.value
});
},
// 协议勾选变化
onAgreementChange(e: any) {
this.setData({
agreedToTerms: e.detail.value.length > 0
});
},
// 发送验证码
async sendCode() {
const phone = this.data.phone;
// 验证手机号
if (!phone || phone.length !== 11) {
wx.showToast({
title: '请输入正确的手机号',
icon: 'none'
});
return;
}
// 手机号格式验证
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(phone)) {
wx.showToast({
title: '手机号格式不正确',
icon: 'none'
});
return;
}
// 防止重复发送
if (this.data.codeSending || this.data.countdown > 0) {
return;
}
// 设置发送状态
this.setData({
codeSending: true
});
try {
console.log('开始发送验证码,手机号:', phone);
// 调用后端API发送验证码showLoading=true显示加载提示
const response = await EmployeeService.sendXHSCode(phone, true);
console.log('发送验证码响应:', response);
if (response.code === 200) {
wx.showToast({
title: '验证码已发送',
icon: 'success'
});
this.startCountdown();
} else {
wx.showToast({
title: response.message || '发送失败',
icon: 'none',
duration: 3000
});
this.setData({
codeSending: false
});
}
} catch (error) {
console.error('发送验证码失败:', error);
// 检查是否是超时错误
const errorMsg = error && typeof error === 'object' && 'message' in error
? (error as any).message
: '发送失败,请重试';
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 3000
});
this.setData({
codeSending: false
});
}
},
// 倒计时
startCountdown() {
let countdown = 180;
this.setData({
countdown: countdown,
codeSending: true
});
const timer = setInterval(() => {
countdown--;
if (countdown <= 0) {
clearInterval(timer);
this.setData({
countdown: 0,
codeSending: false
});
} else {
this.setData({
countdown: countdown
});
}
}, 1000);
},
// 登录
async login() {
const phone = this.data.phone;
const code = this.data.code;
// 检查是否同意协议
if (!this.data.agreedToTerms) {
this.setData({
showAgreementModal: true
});
return;
}
// 验证手机号
if (!phone || phone.length !== 11) {
wx.showToast({
title: '请输入正确的手机号',
icon: 'none'
});
return;
}
// 验证验证码
if (!code || code.length !== 6) {
wx.showToast({
title: '请输入6位验证码',
icon: 'none'
});
return;
}
// 防止重复提交(关键优化)
if (this.data.loginLoading) {
console.log('绑定请求进行中,忽略重复点击');
return;
}
try {
// 设置 loading 状态
this.setData({
loginLoading: true
});
// 调用后端API绑定小红书
const response = await EmployeeService.bindXHS(phone, code);
if (response.code === 200) {
// 绑定成功,保存绑定信息
const bindings = wx.getStorageSync('socialBindings') || {};
bindings.xiaohongshu = {
phone: phone,
bindTime: new Date().getTime(),
xhs_account: (response.data && response.data.xhs_account) || ''
};
wx.setStorageSync('socialBindings', bindings);
wx.showToast({
title: '绑定成功',
icon: 'success',
duration: 1500
});
// 延迟500毫秒后返回让用户看到成功提示
setTimeout(() => {
// 重置 loading 状态
this.setData({
loginLoading: false
});
// 返回上一页(社交账号绑定页)
wx.navigateBack({
delta: 1
});
}, 1500);
} else {
// 绑定失败,重置 loading
this.setData({
loginLoading: false
});
}
} catch (error) {
console.error('绑定小红书失败:', error);
// 重置 loading 状态
this.setData({
loginLoading: false
});
}
},
// 关闭协议弹窗
closeAgreementModal() {
this.setData({
showAgreementModal: false
});
},
// 同意并登录
agreeAndLogin() {
this.setData({
agreedToTerms: true,
showAgreementModal: false
});
// 延迟执行登录,让用户看到勾选效果
setTimeout(() => {
this.login();
}, 300);
},
// 阻止事件冒泡
stopPropagation() {},
// 跳转到用户协议
goToUserAgreement(e: any) {
e.stopPropagation();
wx.navigateTo({
url: '/pages/agreement/user-agreement/user-agreement'
});
},
// 跳转到隐私政策
goToPrivacy(e: any) {
e.stopPropagation();
wx.navigateTo({
url: '/pages/agreement/privacy-policy/privacy-policy'
});
},
// 跳转到个人信息保护政策
goToPersonalInfo(e: any) {
e.stopPropagation();
// TODO: 创建个人信息保护政策页面
wx.showToast({
title: '功能开发中',
icon: 'none'
});
}
});

View File

@@ -1,80 +0,0 @@
<!--pages/profile/xhs-login/xhs-login.wxml-->
<view class="page-container">
<view class="platform-header">
<view class="platform-icon xiaohongshu"></view>
<view class="platform-text">
<text class="platform-name">小红书</text>
<text class="platform-tip">绑定后可同步发布内容</text>
</view>
</view>
<!-- 未绑定状态 -->
<view class="bind-form">
<!-- 手机号 -->
<view class="form-item">
<view class="phone-input-wrapper">
<view class="country-code" bindtap="selectCountryCode">
<text class="code-text">{{countryCode}}</text>
<text class="arrow-down">▼</text>
</view>
<input
class="phone-input"
type="number"
maxlength="11"
placeholder="请输入手机号"
value="{{phone}}"
bindinput="onPhoneInput"
/>
</view>
</view>
<!-- 验证码 -->
<view class="form-item">
<view class="code-input-wrapper">
<input
class="code-input"
type="number"
maxlength="6"
placeholder="请输入验证码"
value="{{code}}"
bindinput="onCodeInput"
/>
<view
class="send-code-text {{codeSending ? 'disabled' : ''}}"
bindtap="sendCode"
>
{{countdown > 0 ? '重新发送(' + countdown + 's)' : '获取验证码'}}
</view>
</view>
</view>
<button class="bind-btn" bindtap="login">
确认
</button>
<!-- 协议提示 -->
<view class="agreement-tip">
<text class="tip-icon">⚠️</text>
<text class="tip-text">我已阅读并同意</text>
<text class="tip-link" bindtap="goToUserAgreement">《用户协议》</text>
<text class="tip-text">、</text>
<text class="tip-link" bindtap="goToPrivacy">《隐私政策》</text>
<text class="tip-text">、</text>
<text class="tip-link" bindtap="goToPersonalInfo">《个人信息保护政策》</text>
</view>
</view>
</view>
<!-- 提示弹窗 -->
<view class="modal-mask" wx:if="{{showAgreementModal}}" bindtap="closeAgreementModal">
<view class="modal-dialog" catchtap="stopPropagation">
<view class="modal-title">温馨提示</view>
<view class="modal-content">
请先同意《用户协议》和《隐私政策》后再登录
</view>
<view class="modal-footer">
<button class="modal-btn cancel" bindtap="closeAgreementModal">不同意</button>
<button class="modal-btn confirm" bindtap="agreeAndLogin">同意</button>
</view>
</view>
</view>

View File

@@ -1,256 +0,0 @@
/* pages/profile/xhs-login/xhs-login.wxss */
page {
background: #f8f8f8;
height: 100%;
}
.page-container {
padding: 30rpx;
min-height: 100vh;
box-sizing: border-box;
}
/* 平台头部 */
.platform-header {
background: white;
border-radius: 16rpx;
padding: 40rpx;
margin-bottom: 30rpx;
display: flex;
align-items: center;
gap: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.platform-icon {
width: 120rpx;
height: 120rpx;
border-radius: 20rpx;
flex-shrink: 0;
}
.platform-icon.xiaohongshu {
background: #07c160;
}
.platform-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.platform-name {
font-size: 36rpx;
font-weight: bold;
color: #1a1a1a;
}
.platform-tip {
font-size: 26rpx;
color: #999;
}
/* 表单 */
.bind-form {
background: white;
border-radius: 16rpx;
padding: 40rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.form-item {
margin-bottom: 32rpx;
}
.form-item:last-of-type {
margin-bottom: 0;
}
/* 手机号输入 */
.phone-input-wrapper {
display: flex;
align-items: center;
height: 88rpx;
background: #f8f8f8;
border-radius: 12rpx;
overflow: hidden;
}
.country-code {
display: flex;
align-items: center;
gap: 8rpx;
padding: 0 20rpx;
height: 100%;
border-right: 1rpx solid #e0e0e0;
flex-shrink: 0;
}
.code-text {
font-size: 28rpx;
color: #1a1a1a;
font-weight: 500;
}
.arrow-down {
font-size: 20rpx;
color: #999;
}
.phone-input {
flex: 1;
height: 100%;
padding: 0 24rpx;
font-size: 28rpx;
color: #1a1a1a;
}
/* 验证码输入 */
.code-input-wrapper {
display: flex;
align-items: center;
height: 88rpx;
background: #f8f8f8;
border-radius: 12rpx;
overflow: hidden;
}
.code-input {
flex: 1;
height: 100%;
padding: 0 24rpx;
font-size: 28rpx;
color: #1a1a1a;
}
.send-code-text {
padding: 0 24rpx;
height: 100%;
display: flex;
align-items: center;
font-size: 26rpx;
color: #07c160;
white-space: nowrap;
flex-shrink: 0;
}
.send-code-text.disabled {
color: #999;
}
.bind-btn {
width: 100%;
height: 88rpx;
background: #07c160;
color: white;
border: none;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
margin-top: 40rpx;
margin-bottom: 24rpx;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.bind-btn::after {
border: none;
}
/* 协议提示 */
.agreement-tip {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 4rpx;
padding: 0 8rpx;
}
.tip-icon {
font-size: 24rpx;
margin-right: 4rpx;
}
.tip-text {
font-size: 22rpx;
color: #999;
line-height: 1.6;
}
.tip-link {
font-size: 22rpx;
color: #07c160;
line-height: 1.6;
}
/* 提示弹窗 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-dialog {
width: 560rpx;
background: white;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
}
.modal-title {
padding: 40rpx 32rpx 24rpx;
font-size: 32rpx;
font-weight: bold;
color: #1a1a1a;
text-align: center;
}
.modal-content {
padding: 0 32rpx 40rpx;
font-size: 28rpx;
color: #666;
text-align: center;
line-height: 1.6;
}
.modal-footer {
display: flex;
border-top: 1rpx solid #e0e0e0;
}
.modal-btn {
flex: 1;
height: 96rpx;
background: white;
border: none;
font-size: 32rpx;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.modal-btn::after {
border: none;
}
.modal-btn.cancel {
color: #666;
border-right: 1rpx solid #e0e0e0;
}
.modal-btn.confirm {
color: #07c160;
font-weight: 600;
}

View File

@@ -1,94 +0,0 @@
<!--pages/profile/xhs-publish/xhs-publish.wxml-->
<view class="page-container">
<!-- 渐变红背景 + 动画效果 -->
<view class="background">
<view class="gradient-bg"></view>
<view class="circle circle1"></view>
<view class="circle circle2"></view>
<view class="circle circle3"></view>
</view>
<!-- 发布卡片 - 毛玻璃效果 -->
<view class="publish-card">
<view class="card-header">
<text class="header-title">✨ 发布小红书笔记</text>
<text class="header-subtitle">分享你的精彩时刻</text>
</view>
<!-- 标题输入 -->
<view class="input-group">
<view class="input-label">
<text class="label-icon">📝</text>
<text class="label-text">标题</text>
</view>
<input
class="input-field"
type="text"
placeholder="给你的笔记起个吸引人的标题"
value="{{title}}"
bindinput="onTitleInput"
maxlength="30"
/>
<view class="char-count">{{title.length}}/30</view>
</view>
<!-- 内容输入 -->
<view class="input-group">
<view class="input-label">
<text class="label-icon">✍️</text>
<text class="label-text">正文内容</text>
</view>
<textarea
class="textarea-field"
placeholder="分享你的故事、经验或想法..."
value="{{content}}"
bindinput="onContentInput"
maxlength="1000"
auto-height
></textarea>
<view class="char-count">{{content.length}}/1000</view>
</view>
<!-- 话题标签 -->
<view class="input-group">
<view class="input-label">
<text class="label-icon">🏷️</text>
<text class="label-text">话题标签</text>
<text class="label-tip">(可选)</text>
</view>
<input
class="input-field"
type="text"
placeholder="输入话题后按空格分隔,如:旅行 美食 生活"
value="{{topicsText}}"
bindinput="onTopicsInput"
/>
<view class="topics-preview" wx:if="{{topics.length > 0}}">
<block wx:for="{{topics}}" wx:key="index">
<view class="topic-tag">#{{item}}#</view>
</block>
</view>
</view>
<!-- 提示信息 -->
<view class="tips-box">
<text class="tips-icon">💡</text>
<text class="tips-text">发布前请确保已登录小红书账号</text>
</view>
<!-- 发布按钮 -->
<button
class="publish-btn {{publishing ? 'disabled' : ''}}"
bindtap="handlePublish"
disabled="{{publishing}}"
>
<text wx:if="{{!publishing}}">🚀 立即发布</text>
<text wx:else>发布中...</text>
</button>
</view>
<!-- Toast提示 -->
<view class="toast {{showToast ? 'show' : ''}}" wx:if="{{toastMessage}}">
<text>{{toastMessage}}</text>
</view>
</view>

View File

@@ -72,9 +72,10 @@ export interface PublishRecord {
export class EmployeeService {
/**
* 发送小红书验证码
* 返回 session_id 用于后续复用浏览器
*/
static async sendXHSCode(xhsPhone: string, showLoading = true) {
return post(API.xhs.sendCode, {
return post<{ sent_at: string; session_id: string }>(API.xhs.sendCode, {
xhs_phone: xhsPhone
}, showLoading);
}
@@ -88,11 +89,16 @@ export class EmployeeService {
/**
* 绑定小红书账号
* @param xhsPhone 小红书手机号
* @param code 验证码
* @param sessionId 发送验证码时返回的session_id用于复用浏览器
* @param showLoading 是否显示加载动画
*/
static async bindXHS(xhsPhone: string, code: string, showLoading = false) {
static async bindXHS(xhsPhone: string, code: string, sessionId?: string, showLoading = false) {
return post<{ xhs_account: string }>(API.employee.bindXHS, {
xhs_phone: xhsPhone,
code
code,
session_id: sessionId // 传递session_id
}, showLoading);
}

View File

@@ -51,8 +51,8 @@ export function request<T = any>(options: RequestOptions): Promise<ApiResponse<T
const response = res.data as ApiResponse<T>;
// 处理业务状态码
if (response.code === 200) {
// 处理业务状态码(兼容 code=0 和 code=200
if (response.code === 200 || response.code === 0) {
resolve(response);
} else if (response.code === 401) {
// Token过期或未登录