This commit is contained in:
sjk
2026-01-06 19:36:42 +08:00
parent 15b579d64a
commit 19942144fb
261 changed files with 24034 additions and 5477 deletions

View File

@@ -1,13 +1,10 @@
{
"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",
@@ -15,9 +12,7 @@
"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",
@@ -25,32 +20,6 @@
"navigationBarBackgroundColor": "#07c160",
"backgroundColor": "#f5f5f5"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#07c160",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/home/home",
"text": "首页",
"iconPath": "images/tabbar/home.png",
"selectedIconPath": "images/tabbar/home-active.png"
},
{
"pagePath": "pages/profile/published/published",
"text": "我的发布",
"iconPath": "images/tabbar/article.png",
"selectedIconPath": "images/tabbar/article-active.png"
},
{
"pagePath": "pages/profile/profile",
"text": "我的",
"iconPath": "images/tabbar/user.png",
"selectedIconPath": "images/tabbar/user-active.png"
}
]
},
"style": "v2",
"lazyCodeLoading": "requiredComponents"
}

View File

@@ -3,7 +3,7 @@ import { API } from './config/api';
interface IAppInstance {
globalData: Record<string, any>;
showEnvironmentTip: () => void;
logEnvironmentInfo: () => void;
}
App<IAppOption & IAppInstance>({
@@ -14,8 +14,8 @@ App<IAppOption & IAppInstance>({
logs.unshift(Date.now())
wx.setStorageSync('logs', logs)
// 显示当前环境提示
this.showEnvironmentTip();
// 输出环境信息到控制台(不显示弹窗)
this.logEnvironmentInfo();
// 登录
wx.login({
@@ -26,27 +26,23 @@ App<IAppOption & IAppInstance>({
})
},
// 显示环境提示
showEnvironmentTip() {
// 输出环境信息到控制台
logEnvironmentInfo() {
try {
const accountInfo = wx.getAccountInfoSync();
const envVersion = accountInfo.miniProgram.envVersion;
let envName = '';
let envColor = '#07c160';
switch (envVersion) {
case 'develop':
envName = '开发环境';
envColor = '#52c41a';
break;
case 'trial':
envName = '体验环境';
envColor = '#faad14';
break;
case 'release':
envName = '生产环境';
envColor = '#1677ff';
break;
default:
envName = '未知环境';
@@ -55,17 +51,6 @@ App<IAppOption & IAppInstance>({
// 输出到控制台
console.log(`[App] 当前环境: ${envName}`);
console.log(`[App] 后端地址: ${API.baseURL}`);
// 显示弹窗提示 (仅开发和体验环境)
if (envVersion === 'develop' || envVersion === 'trial') {
wx.showModal({
title: '环境提示',
content: `当前运行在${envName}\n后端地址: ${API.baseURL}`,
showCancel: false,
confirmText: '知道了',
confirmColor: envColor
});
}
} catch (error) {
console.error('[App] 获取环境信息失败:', error);
}

View File

@@ -25,8 +25,8 @@ interface EnvConfig {
const API_CONFIG: Record<EnvType, EnvConfig> = {
// 开发环境 - 本地开发
dev: {
baseURL: 'http://192.168.17.127:8080', // 本地Go服务
pythonURL: 'http://192.168.17.127:8000', // 本地Python服务
baseURL: 'http://localhost:8080', // 本地Go服务
pythonURL: 'http://localhost:8000', // 本地Python服务
timeout: 90000
},
@@ -77,6 +77,13 @@ function detectEnvironment(): EnvType {
const currentEnv: EnvType = detectEnvironment();
const currentConfig = (): EnvConfig => API_CONFIG[currentEnv];
// 导出环境检测相关函数和变量
export const getCurrentEnv = (): EnvType => currentEnv;
export const isDevelopment = (): boolean => currentEnv === 'dev';
export const isDevOrTrial = (): boolean => currentEnv === 'dev' || currentEnv === 'test'; // 开发版或体验版
export { detectEnvironment };
export type { EnvType };
// 输出环境信息
console.log(`[API Config] 当前环境: ${currentEnv}`);
console.log(`[API Config] 主服务: ${currentConfig().baseURL}`);
@@ -99,14 +106,17 @@ export const API = {
// 登录接口
auth: {
wechatLogin: '/api/login/wechat', // 微信登录
phoneLogin: '/api/login/phone' // 手机号登录
wechatLogin: '/api/login/wechat', // 微信登录
phoneLogin: '/api/login/phone', // 手机号登录
phonePasswordLogin: '/api/login/phone-password', // 手机号密码登录
xhsPhoneCodeLogin: '/api/login/xhs-phone-code' // 小红书手机号验证码登录
},
// 员工端接口
employee: {
profile: '/api/employee/profile', // 获取个人信息
bindXHS: '/api/employee/bind-xhs', // 绑定小红书
bindXHSStatus: '/api/employee/bind-xhs-status', // 获取绑定状态
unbindXHS: '/api/employee/unbind-xhs', // 解绑小红书
availableCopies: '/api/employee/available-copies', // 获取可领取文案
claimCopy: '/api/employee/claim-copy', // 领取文案
@@ -122,7 +132,8 @@ export const API = {
// 小红书相关接口
xhs: {
sendCode: '/api/xhs/send-code' // 发送小红书验证码
sendCode: '/api/xhs/send-code', // 发送小红书验证码
sendVerificationCode: '/api/xhs/send-verification-code' // 发送验证码(新接口)
}
};

View File

@@ -0,0 +1,5 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" fill="#E5E5E5"/>
<circle cx="50" cy="35" r="15" fill="#999999"/>
<path d="M25 75C25 65 35 58 50 58C65 58 75 65 75 75" stroke="#999999" stroke-width="8" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -0,0 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.25 3C11.5931 3 10.25 4.34315 10.25 6V42C10.25 43.6569 11.5931 45 13.25 45H34.75C36.4069 45 37.75 43.6569 37.75 42V6C37.75 4.34315 36.4069 3 34.75 3H13.25ZM13.25 6H34.75V42H13.25V6ZM24 38.5C22.6193 38.5 21.5 39.6193 21.5 41C21.5 42.3807 22.6193 43.5 24 43.5C25.3807 43.5 26.5 42.3807 26.5 41C26.5 39.6193 25.3807 38.5 24 38.5Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 462 B

View File

@@ -0,0 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 12.5C11.15 12.5 6 17.1 6 22.8C6 26.1 7.65 29 10.2 31L9 35.5L13.5 33.2C14.8 33.7 16.1 34 17.5 34C23.85 34 29 29.4 29 23.7C29 18.1 23.85 12.5 17.5 12.5ZM14.5 26C13.12 26 12 24.88 12 23.5C12 22.12 13.12 21 14.5 21C15.88 21 17 22.12 17 23.5C17 24.88 15.88 26 14.5 26ZM20.5 26C19.12 26 18 24.88 18 23.5C18 22.12 19.12 21 20.5 21C21.88 21 23 22.12 23 23.5C23 24.88 21.88 26 20.5 26ZM31.5 22C31.2 22 30.9 22 30.6 22.1C31.5 19.9 31.8 17.5 31.3 15.2C37.2 16.6 41.5 21.2 41.5 26.7C41.5 29.3 40.2 31.7 38.1 33.4L39 37.5L35.1 35.6C34 36.05 32.25 36.4 31.5 36.4C26.15 36.4 21.7 32.5 21.7 27.7C21.7 24.4 24 21.6 27.5 20.1C28.7 20.9 30.1 21.4 31.5 21.4V22ZM27.5 30C26.12 30 25 28.88 25 27.5C25 26.12 26.12 25 27.5 25C28.88 25 30 26.12 30 27.5C30 28.88 28.88 30 27.5 30ZM35.5 30C34.12 30 33 28.88 33 27.5C33 26.12 34.12 25 35.5 25C36.88 25 38 26.12 38 27.5C38 28.88 36.88 30 35.5 30Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1004 B

View File

@@ -1,4 +1,9 @@
{
"navigationBarTitleText": "首页",
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5",
"backgroundTextStyle": "dark",
"usingComponents": {}
}

View File

@@ -1,5 +1,5 @@
// pages/articles/articles.ts
import { formatDate, getStatusInfo, getChannelInfo, getCoverColor, getCoverIcon } from '../../utils/util'
import { formatDate, getStatusInfo, getChannelInfo, getCoverColor, getCoverIcon, getImageUrl } from '../../utils/util'
import { EmployeeService } from '../../services/employee'
Page({
@@ -13,7 +13,13 @@ Page({
loading: false,
claiming: false, // 领取中
publishing: false, // 发布中
rejecting: false // 拒绝中
rejecting: false, // 拒绝中
hasLoaded: false, // 是否已加载过数据
// 图片拖拽状态
draggingId: null as number | null, // 当前拖拽的图片ID
dragStartX: 0, // 开始拖拽的X坐标
dragStartIndex: -1, // 开始拖拽的索引
isDragging: false // 是否正在拖拽
},
onLoad(options: any) {
@@ -38,7 +44,7 @@ Page({
}
// 检查小红书绑定状态
this.checkXHSBinding().then(isBound => {
this.checkXHSBinding().then((isBound: boolean) => {
if (!isBound) {
wx.showModal({
title: '未绑定小红书',
@@ -83,14 +89,19 @@ Page({
},
onShow() {
// 每次显示页面时刷新数据
if (this.data.productId) {
// 仅在未加载过数据时加载(例如从其他页面返回且数据已存在)
if (this.data.productId && !this.data.hasLoaded) {
this.loadCopies();
}
},
// 下拉刷新
onPullDownRefresh() {
this.loadCopies(true);
},
// 加载文案列表
async loadCopies() {
async loadCopies(isPullRefresh: boolean = false) {
const productId = this.data.productId;
if (!productId) {
@@ -98,6 +109,9 @@ Page({
title: '请先选择产品',
icon: 'none'
});
if (isPullRefresh) {
wx.stopPullDownRefresh();
}
return;
}
@@ -121,22 +135,49 @@ Page({
console.log('标签数量:', (copies[0].tags && copies[0].tags.length) || 0);
}
// 处理图片URL使用统一工具函数处理
const processedCopies = copies.map((copy: any) => {
if (copy.images && Array.isArray(copy.images)) {
copy.images = copy.images.map((img: any) => {
return {
...img,
image_url: getImageUrl(img.image_url),
image_thumb_url: getImageUrl(img.image_thumb_url || img.image_url)
};
});
}
return copy;
});
// 确保 currentCopy 有值
const firstCopy = processedCopies.length > 0 ? processedCopies[0] : null;
console.log('即将设置 currentCopy:', firstCopy);
this.setData({
allCopies: copies,
allCopies: processedCopies,
productName: product.name || '',
productImage: product.image || '',
currentIndex: 0,
currentCopy: copies.length > 0 ? copies[0] : null,
loading: false
currentCopy: firstCopy,
loading: false,
hasLoaded: true // 标记已加载
});
console.log('setData后的 currentCopy:', this.data.currentCopy);
console.log('allCopies 长度:', this.data.allCopies.length);
if (copies.length === 0) {
wx.showToast({
title: '暂无可领取文案',
icon: 'none'
});
} else if (isPullRefresh) {
wx.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
}
}
} catch (error) {
@@ -146,6 +187,10 @@ Page({
title: '加载文案失败',
icon: 'none'
});
} finally {
if (isPullRefresh) {
wx.stopPullDownRefresh();
}
}
},
@@ -163,6 +208,15 @@ Page({
return;
}
// 如果只有一篇文案,提示用户
if (allCopies.length === 1) {
wx.showToast({
title: '已经是唯一的文案了',
icon: 'none'
});
return;
}
// 显示加载动画
wx.showLoading({
title: '加载中...',
@@ -173,18 +227,24 @@ Page({
setTimeout(() => {
// 切换到下一个文案
const nextIndex = (currentIndex + 1) % allCopies.length;
const nextCopy = allCopies[nextIndex];
this.setData({
currentIndex: nextIndex,
currentCopy: allCopies[nextIndex]
});
// 确保 nextCopy 存在且有效
if (nextCopy) {
this.setData({
currentIndex: nextIndex,
currentCopy: nextCopy
});
} else {
console.error('换一换失败: nextCopy 为空', { nextIndex, allCopies });
}
// 隐藏加载动画
wx.hideLoading();
}, 300);
},
// 一键发布(先领取,再发布)
// 一键发布(先检查绑定状态,再领取,再发布)
async publishArticle() {
if (!this.data.currentCopy) {
wx.showToast({
@@ -194,6 +254,54 @@ Page({
return;
}
// 验证内容是否为空
const { currentCopy } = this.data;
const title = (currentCopy.title || '').trim();
const content = (currentCopy.content || '').trim();
const images = currentCopy.images || [];
if (!title) {
wx.showToast({
title: '请输入标题',
icon: 'none'
});
return;
}
if (!content) {
wx.showToast({
title: '请输入文案内容',
icon: 'none'
});
return;
}
if (images.length === 0) {
wx.showToast({
title: '请至少添加一张图片',
icon: 'none'
});
return;
}
// 发布前检查小红书绑定状态
const isBound = await this.checkXHSBinding();
if (!isBound) {
wx.showModal({
title: '未绑定小红书',
content: '发布内容前需要先绑定小红书账号',
confirmText: '去绑定',
success: (res) => {
if (res.confirm) {
wx.navigateTo({
url: '/pages/profile/platform-bind/platform-bind'
});
}
}
});
return;
}
wx.showModal({
title: '确认发布',
content: '确定要领取并发布这篇文案吗?',
@@ -212,9 +320,23 @@ Page({
this.setData({ claiming: true });
try {
// 1. 先领取文案
const { currentCopy } = this.data;
// 1. 先保存修改的内容
try {
await EmployeeService.updateArticleContent(currentCopy.id, {
title: currentCopy.title,
content: currentCopy.content
});
console.log('文案内容已保存');
} catch (error) {
console.error('保存文案失败:', error);
// 继续发布,不阻断流程
}
// 2. 领取文案
const claimResponse = await EmployeeService.claimCopy(
this.data.currentCopy.id,
currentCopy.id,
this.data.productId
);
@@ -227,16 +349,39 @@ Page({
this.setData({ claiming: false, publishing: true });
// 2. 发布
// 3. 发布(使用修改后的内容)
const publishResponse = await EmployeeService.publish({
copy_id: this.data.currentCopy.id,
title: (copyData && copyData.title) || this.data.currentCopy.title,
content: (copyData && copyData.content) || this.data.currentCopy.content,
copy_id: currentCopy.id,
title: currentCopy.title, // 使用修改后的标题
content: currentCopy.content, // 使用修改后的内容
publish_link: '',
xhs_note_id: ''
});
if (publishResponse.code === 200) {
// 发布成功后,同步更新发布记录的完整内容(标题、内容、图片、标签)
try {
const recordId = publishResponse.data && publishResponse.data.record_id;
if (recordId) {
const images = (currentCopy.images || []).map((img: any, index: number) => ({
image_url: img.image_url,
image_thumb_url: img.image_thumb_url || img.image_url,
sort_order: img.sort_order || index + 1,
keywords_name: img.keywords_name || '产品图片'
}));
await EmployeeService.updatePublishRecord(recordId, {
title: currentCopy.title,
content: currentCopy.content,
images,
tags: currentCopy.tags || []
});
}
} catch (err) {
console.error('同步发布记录失败:', err);
// 不阻断发布成功,只记录错误
}
wx.showToast({
title: '发布成功',
icon: 'success',
@@ -372,5 +517,272 @@ Page({
title: `${this.data.productName} - 精彩种草文案`,
imageUrl: this.data.productImage || ''
};
},
// 修改标题
onTitleInput(e: any) {
const { currentCopy, currentIndex, allCopies } = this.data;
// 直接更新当前文案的标题
currentCopy.title = e.detail.value;
allCopies[currentIndex] = currentCopy;
this.setData({
currentCopy,
allCopies
});
},
// 修改内容
onContentInput(e: any) {
const { currentCopy, currentIndex, allCopies } = this.data;
// 直接更新当前文案的内容
currentCopy.content = e.detail.value;
allCopies[currentIndex] = currentCopy;
this.setData({
currentCopy,
allCopies
});
},
// ========== 图片管理功能 ==========
// 选择并上传图片
async chooseAndUploadImage() {
console.log('点击添加图片按钮');
const { currentCopy, currentIndex, allCopies } = this.data;
if (!currentCopy) {
wx.showToast({
title: '请先选择文案',
icon: 'none'
});
return;
}
try {
console.log('开始选择图片...');
// 1. 选择图片
const res = await wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed']
}) as any;
console.log('选择图片结果:', res);
if (!res.tempFiles || res.tempFiles.length === 0) {
console.error('未选择图片');
return;
}
const tempFilePath = res.tempFiles[0].tempFilePath;
console.log('临时文件路径:', tempFilePath);
wx.showLoading({ title: '上传中...', mask: true });
// 2. 上传到服务器(仅上传文件,不修改文案数据)
console.log('开始上传图片...');
const uploadRes = await EmployeeService.uploadImage(tempFilePath);
console.log('上传响应:', uploadRes);
if (uploadRes.code === 200 && uploadRes.data) {
// 3. 仅在本地更新当前文案的图片列表
const images = currentCopy.images ? [...currentCopy.images] : [];
const newImage = {
id: Date.now(), // 本地唯一ID
image_url: getImageUrl(uploadRes.data.image_url),
image_thumb_url: getImageUrl(uploadRes.data.image_thumb_url || uploadRes.data.image_url),
sort_order: images.length + 1,
keywords_name: '产品图片'
} as any;
images.push(newImage);
currentCopy.images = images;
allCopies[currentIndex] = currentCopy;
this.setData({
currentCopy,
allCopies
});
wx.hideLoading();
wx.showToast({ title: '添加成功', icon: 'success' });
} else {
throw new Error(uploadRes.message || '上传失败');
}
} catch (error: any) {
console.error('添加图片失败:', error);
wx.hideLoading();
// 处理用户取消选择的情况
if (error.errMsg && error.errMsg.includes('cancel')) {
console.log('用户取消选择图片');
return;
}
wx.showToast({
title: error.message || error.errMsg || '上传失败',
icon: 'none'
});
}
},
// 预览图片
previewImage(e: any) {
const { url } = e.currentTarget.dataset;
const { currentCopy } = this.data;
if (!currentCopy || !currentCopy.images) return;
const urls = currentCopy.images.map((img: any) => img.image_url);
wx.previewImage({
current: url,
urls: urls
});
},
// 删除图片(仅本地修改,未发布前不更新后端)
async deleteImage(e: any) {
const { id } = e.currentTarget.dataset;
const { currentCopy, currentIndex, allCopies } = this.data;
if (!currentCopy || !currentCopy.images) {
return;
}
// 检查是否是最后一张
if (currentCopy.images.length <= 1) {
wx.showToast({
title: '至少需要保留一张图片',
icon: 'none'
});
return;
}
const res = await wx.showModal({
title: '确认删除',
content: '确定要删除这张图片吗?'
});
if (res.confirm) {
try {
const imageId = Number(id);
let images = currentCopy.images.filter((img: any) => Number(img.id) !== imageId);
if (images.length === 0) {
wx.showToast({
title: '至少需要保留一张图片',
icon: 'none'
});
return;
}
// 重新计算排序
images = images.map((img: any, index: number) => ({
...img,
sort_order: index + 1
}));
currentCopy.images = images;
allCopies[currentIndex] = currentCopy;
this.setData({
currentCopy,
allCopies
});
} catch (error: any) {
wx.showToast({ title: error.message || '删除失败', icon: 'none' });
}
}
},
// ========== 图片拖拽功能 ==========
// 长按图片开始拖拽
onImageLongPress(e: any) {
const { id, index } = e.currentTarget.dataset;
console.log('长按图片:', id, index);
// 振动反馈
wx.vibrateShort({ type: 'medium' });
this.setData({
draggingId: Number(id),
dragStartIndex: index,
isDragging: true
});
wx.showToast({
title: '已进入拖拽模式',
icon: 'none',
duration: 1000
});
},
// 触摸开始
onImageTouchStart(e: any) {
if (!this.data.isDragging) return;
this.setData({
dragStartX: e.touches[0].pageX
});
},
// 触摸移动
onImageTouchMove(e: any) {
if (!this.data.isDragging || !this.data.draggingId) return;
const { dragStartX, dragStartIndex, currentCopy, currentIndex, allCopies } = this.data;
const currentX = e.touches[0].pageX;
const deltaX = currentX - dragStartX;
// 每移动176rpx160rpx图片 + 16rpx间隙切换一个位置
const itemWidth = 176 * (wx.getSystemInfoSync().windowWidth / 750); // rpx转px
const moveSteps = Math.round(deltaX / itemWidth);
if (moveSteps !== 0) {
const newIndex = dragStartIndex + moveSteps;
if (newIndex >= 0 && newIndex < currentCopy.images.length) {
// 交换图片位置
const images = [...currentCopy.images];
const draggedItem = images[dragStartIndex];
images.splice(dragStartIndex, 1);
images.splice(newIndex, 0, draggedItem);
// 更新sort_order
images.forEach((img, idx) => {
img.sort_order = idx + 1;
});
currentCopy.images = images;
allCopies[currentIndex] = currentCopy;
this.setData({
currentCopy,
allCopies,
dragStartIndex: newIndex,
dragStartX: currentX
});
// 振动反馈
wx.vibrateShort({ type: 'light' });
}
}
},
// 触摸结束
onImageTouchEnd(e: any) {
if (!this.data.isDragging) return;
this.setData({
draggingId: null,
dragStartX: 0,
dragStartIndex: -1,
isDragging: false
});
wx.showToast({
title: '图片顺序已调整',
icon: 'success',
duration: 1000
});
}
});

View File

@@ -3,10 +3,40 @@
<!-- 文案内容 -->
<scroll-view class="article-container" scroll-y enable-flex wx:if="{{currentCopy}}">
<view class="article-wrapper">
<!-- 文章图片 -->
<view class="article-images" wx:if="{{currentCopy.images && currentCopy.images.length > 0}}">
<view class="image-item" wx:for="{{currentCopy.images}}" wx:key="id">
<image class="article-image" src="{{item.image_url || item}}" mode="aspectFill" />
<!-- 文章图片画廊 -->
<view class="article-images">
<view class="image-gallery">
<!-- 已有图片 -->
<view
class="image-item {{draggingId === item.id ? 'dragging' : ''}}"
wx:for="{{currentCopy.images}}"
wx:key="id"
data-id="{{item.id}}"
data-index="{{index}}"
bindlongpress="onImageLongPress"
bindtouchstart="onImageTouchStart"
bindtouchmove="onImageTouchMove"
bindtouchend="onImageTouchEnd"
style="{{draggingId === item.id ? 'opacity: 0.5; transform: scale(1.05);' : ''}}">
<image
class="gallery-image"
src="{{item.image_url}}"
mode="aspectFill"
bindtap="previewImage"
data-url="{{item.image_url}}"
/>
<view class="delete-btn" catchtap="deleteImage" data-id="{{item.id}}">
<!-- 绿色圆形删除按钮 -->
<view class="delete-icon">
<view class="delete-icon-line line-1"></view>
<view class="delete-icon-line line-2"></view>
</view>
</view>
</view>
<!-- 添加按钮 -->
<view class="image-item add-btn" bindtap="chooseAndUploadImage">
<text class="add-icon">+</text>
</view>
</view>
</view>
@@ -18,22 +48,27 @@
</view>
<view class="article-header">
<text class="article-title">{{currentCopy.title}}</text>
<!-- 标题始终可编辑 -->
<input
class="article-title-input"
placeholder="请输入标题最多20字"
value="{{currentCopy.title}}"
bindinput="onTitleInput"
maxlength="20"
confirm-type="done"
/>
</view>
<view class="article-content">
<text class="content-text">{{currentCopy.content}}</text>
</view>
<view class="article-meta">
<view class="meta-item">
<text class="meta-label">主题:</text>
<text class="meta-value">{{currentCopy.topic}}</text>
</view>
<view class="meta-item">
<text class="meta-label">字数:</text>
<text class="meta-value">{{currentCopy.content.length}}字</text>
</view>
<!-- 内容始终可编辑 -->
<textarea
class="content-textarea"
placeholder="请输入内容最多1000字"
value="{{currentCopy.content}}"
bindinput="onContentInput"
maxlength="1000"
show-confirm-bar="{{false}}"
/>
</view>
</view>
</scroll-view>
@@ -55,22 +90,14 @@
<button
class="action-btn refresh"
bindtap="changeArticle"
disabled="{{claiming || publishing || rejecting}}"
disabled="{{claiming || publishing}}"
>
<text class="btn-text">换一换</text>
</button>
<button
class="action-btn reject"
bindtap="rejectArticle"
disabled="{{claiming || publishing || rejecting}}"
loading="{{rejecting}}"
>
<text class="btn-text">{{rejecting ? '拒绝中...' : '拒绝'}}</text>
</button>
<button
class="action-btn primary"
bindtap="publishArticle"
disabled="{{claiming || publishing || rejecting}}"
disabled="{{claiming || publishing}}"
loading="{{claiming || publishing}}"
>
<text class="btn-text">{{claiming ? '领取中...' : (publishing ? '发布中...' : '一键发布')}}</text>

View File

@@ -9,7 +9,7 @@ page {
display: flex;
flex-direction: column;
box-sizing: border-box;
padding-bottom: calc(136rpx + env(safe-area-inset-bottom));
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
}
/* 文案容器 */
@@ -22,36 +22,118 @@ page {
.article-wrapper {
padding: 30rpx;
padding-bottom: 100rpx;
}
/* 文章图片 - 3列网格布局 */
/* 文章图片画廊 - 水平滚动 */
.article-images {
margin-bottom: 24rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
box-sizing: border-box;
width: 100%;
margin-bottom: 32rpx;
}
.article-images .section-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16rpx;
}
.image-gallery {
display: flex;
gap: 16rpx;
overflow-x: auto;
padding: 16rpx 0 24rpx;
-webkit-overflow-scrolling: touch;
}
.image-gallery::-webkit-scrollbar {
display: none;
}
.image-item {
width: 100%;
padding-bottom: 100%; /* 1:1 比例 */
position: relative;
border-radius: 8rpx;
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
overflow: hidden;
flex-shrink: 0;
background: #f5f5f5;
transition: transform 0.2s, opacity 0.2s;
}
.article-image {
position: absolute;
top: 0;
left: 0;
.image-item:active {
transform: scale(0.98);
}
/* 拖拽状态 */
.image-item.dragging {
opacity: 0.6;
transform: scale(1.1);
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.2);
z-index: 10;
}
.gallery-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 删除按钮 - 占据右上角圆角区域,透明灰色背景 */
.delete-btn {
position: absolute;
top: 0;
right: 0;
width: 48rpx;
height: 48rpx;
border-radius: 0 16rpx 0 16rpx;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.delete-icon {
position: relative;
width: 16rpx;
height: 16rpx;
}
.delete-icon-line {
position: absolute;
left: 50%;
top: 50%;
width: 16rpx;
height: 2rpx;
background: white;
border-radius: 1rpx;
}
.delete-icon-line.line-1 {
transform: translate(-50%, -50%) rotate(45deg);
}
.delete-icon-line.line-2 {
transform: translate(-50%, -50%) rotate(-45deg);
}
/* 添加按钮 */
.image-item.add-btn {
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx dashed #d9d9d9;
}
.add-btn .add-icon {
font-size: 48rpx;
color: #999;
font-weight: 300;
line-height: 1;
}
/* 标签样式 */
.article-tags {
display: flex;
@@ -74,24 +156,61 @@ page {
margin-bottom: 24rpx;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
}
.article-title {
.char-count {
position: absolute;
right: 16rpx;
bottom: 32rpx;
font-size: 24rpx;
color: #999;
background: rgba(255, 255, 255, 0.9);
padding: 4rpx 12rpx;
border-radius: 12rpx;
transition: color 0.3s;
}
.char-count-warning {
color: #ff4d4f;
font-weight: 600;
}
/* 标题输入框 - 始终可编辑 */
.article-title-input {
width: 100%;
font-size: 36rpx;
color: #1a1a1a;
font-weight: bold;
line-height: 1.4;
padding: 0;
border: none;
background: transparent;
box-sizing: border-box;
height: 50rpx;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
}
.article-content {
margin-bottom: 24rpx;
position: relative;
}
.content-text {
/* 内容输入框 - 始终可编辑 */
.content-textarea {
width: 100%;
font-size: 28rpx;
color: #333;
line-height: 1.8;
white-space: pre-line;
padding: 0;
border: none;
background: transparent;
box-sizing: border-box;
/* 动态计算高度100vh - 图片区域 - 标签 - 标题 - 底部按钮栏 - 其他间距 */
min-height: calc(100vh - 450rpx);
max-height: calc(100vh - 300rpx);
}
.article-meta {
@@ -198,9 +317,25 @@ page {
border: none;
}
/* 保存按钮 */
.action-btn.save-btn {
flex: 0 0 160rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
color: white;
}
.action-btn.save-btn:active {
opacity: 0.9;
}
.action-btn.save-btn[disabled] {
background: #d9d9d9;
color: #999;
}
/* 换一换按钮 */
.action-btn.refresh {
flex: 0 0 220rpx;
flex: 1;
background: #f5f5f5;
color: #333;
}
@@ -209,17 +344,6 @@ page {
background: #e8e8e8;
}
/* 拒绝按钮 */
.action-btn.reject {
flex: 0 0 180rpx;
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
color: white;
}
.action-btn.reject:active {
opacity: 0.9;
}
/* 一键发布按钮 */
.action-btn.primary {
flex: 1;

View File

@@ -1,5 +1,8 @@
{
"navigationBarTitleText": "商品选择",
"navigationBarBackgroundColor": "#07c160",
"navigationBarTextStyle": "white"
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5",
"backgroundTextStyle": "light"
}

View File

@@ -1,5 +1,6 @@
// pages/home/home.ts
import { EmployeeService, Product as ApiProduct } from '../../services/employee';
import { getImageUrl } from '../../utils/util';
interface Product {
id: number;
@@ -27,187 +28,158 @@ Page({
hasMore: true,
loading: false,
page: 1,
pageSize: 6
pageSize: 6,
userAvatar: '/images/default-avatar.svg', // 默认头像
avatarX: 9999, // 头像 X 坐标(设置到屏幕外,等待初始化)
avatarY: 9999, // 头像 Y 坐标
windowWidth: 375, // 窗口宽度
windowHeight: 667, // 窗口高度
avatarAnimation: false // 是否启用头像动画
},
onLoad() {
// 不在onLoad检查登录允许用户浏览首页
this.loadProducts();
this.loadUserAvatar();
this.initAvatarPosition();
},
onShow() {
// 页面显示时重新加载用户头像(处理退出登录后返回的场景)
this.loadUserAvatar();
},
// 初始化头像位置(右下角)
initAvatarPosition() {
const systemInfo = wx.getSystemInfoSync();
const windowWidth = systemInfo.windowWidth;
const windowHeight = systemInfo.windowHeight;
// 尝试从本地存储读取上次位置
const savedPosition = wx.getStorageSync('avatarPosition');
let initX = windowWidth - 100; // 100rpx 头像宽度
let initY = windowHeight - 200; // 留出底部按钮空间
if (savedPosition && savedPosition.x !== undefined && savedPosition.y !== undefined) {
// 使用保存的位置
initX = savedPosition.x;
initY = savedPosition.y;
}
// 同步设置窗口尺寸和位置,确保不触发动画
this.setData({
windowWidth,
windowHeight,
avatarX: initX,
avatarY: initY,
avatarAnimation: false // 明确禁用动画
});
},
// 下拉刷新
onPullDownRefresh() {
// 重置分页数据,重新加载第一页
this.setData({
products: [],
page: 1,
hasMore: true
});
this.loadProducts(true);
},
// 加载用户头像
async loadUserAvatar() {
const token = wx.getStorageSync('token');
if (!token) {
// 未登录,重置为默认头像
this.setData({
userAvatar: '/images/default-avatar.svg'
});
return;
}
try {
const response = await EmployeeService.getProfile();
if (response.code === 200 && response.data) {
const avatar = getImageUrl(response.data.avatar) || '/images/default-avatar.svg';
this.setData({
userAvatar: avatar
});
}
} catch (error) {
console.error('获取用户信息失败:', error);
// 获取失败,重置为默认头像
this.setData({
userAvatar: '/images/default-avatar.svg'
});
}
},
// 加载商品列表
async loadProducts() {
if (this.data.loading || !this.data.hasMore) return;
async loadProducts(isPullRefresh: boolean = false) {
if (this.data.loading || !this.data.hasMore) {
if (isPullRefresh) {
wx.stopPullDownRefresh();
}
return;
}
this.setData({ loading: true });
try {
// 从后端API获取产品列表公开接口,不需要登录
const response = await EmployeeService.getProducts();
// 从后端API获取产品列表支持分页
const { page, pageSize, products } = this.data;
const response = await EmployeeService.getProducts(page, pageSize);
if (response.code === 200 && response.data) {
const apiProducts = response.data.list.map((product: ApiProduct, index: number) => ({
id: product.id,
name: product.name,
price: 0, // 后端暂无价格字段
sales: product.available_copies || 0,
image: product.image || `https://picsum.photos/id/${237 + index}/300/400`,
category: 'beauty', // 后端暂无分类字段
tags: ['种草', '推荐'],
hotLevel: product.available_copies > 5 ? 5 : 3,
description: product.description,
available_copies: product.available_copies
}));
const apiProducts = response.data.list.map((product: ApiProduct, index: number) => {
// 商品名称最多8个字多了直接截断
const truncatedName = product.name.length > 8 ? product.name.substring(0, 8) : product.name;
return {
id: product.id,
name: truncatedName,
price: 0, // 后端暂无价格字段
sales: product.available_copies || 0,
image: getImageUrl(product.image) || `https://picsum.photos/id/${237 + products.length + index}/300/400`,
category: 'beauty', // 后端暂无分类字段
tags: ['种草', '推荐'],
hotLevel: product.available_copies > 5 ? 5 : 3,
description: product.description,
available_copies: product.available_copies
};
});
this.setData({
products: apiProducts,
products: [...products, ...apiProducts],
loading: false,
hasMore: false
hasMore: !!response.data.has_more,
page: page + 1
});
if (isPullRefresh) {
wx.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
}
return;
}
} catch (error) {
console.error('加载产品失败:', error);
// API失败使用模拟数据
} finally {
if (isPullRefresh) {
wx.stopPullDownRefresh();
}
}
// 如果API失败使用模拟数据
const allMockProducts = [
{
id: 1,
name: '兰蔻小黑瓶精华液',
price: 680,
sales: 10234,
image: 'https://picsum.photos/id/237/300/400',
category: 'beauty',
tags: ['美白', '抗老', '保湿'],
hotLevel: 5
},
{
id: 2,
name: 'SK-II神仙水',
price: 1299,
sales: 8765,
image: 'https://picsum.photos/id/152/300/400',
category: 'beauty',
tags: ['补水', '缩毛孔', '提亮'],
hotLevel: 5
},
{
id: 3,
name: '雅诗兰黛小棕瓶',
price: 790,
sales: 9432,
image: 'https://picsum.photos/id/292/300/400',
category: 'beauty',
tags: ['修复', '抗氧化', '紧致'],
hotLevel: 4
},
{
id: 4,
name: 'Dior烈艳蓝金口红',
price: 320,
sales: 15678,
image: 'https://picsum.photos/id/365/300/400',
category: 'beauty',
tags: ['段色', '滑顺', '持久'],
hotLevel: 5
},
{
id: 5,
name: 'AirPods Pro 2',
price: 1899,
sales: 23456,
image: 'https://picsum.photos/id/180/300/400',
category: 'digital',
tags: ['降噪', '音质', '舒适'],
hotLevel: 5
},
{
id: 6,
name: 'iPhone 15 Pro',
price: 7999,
sales: 34567,
image: 'https://picsum.photos/id/119/300/400',
category: 'digital',
tags: ['性能', '拍照', '长续航'],
hotLevel: 5
},
{
id: 7,
name: '海蓝之谜精华面霜',
price: 890,
sales: 7654,
image: 'https://picsum.photos/id/225/300/400',
category: 'beauty',
tags: ['保湿', '修复', '舒缓'],
hotLevel: 4
},
{
id: 8,
name: '迪奥烈影精华',
price: 1680,
sales: 5432,
image: 'https://picsum.photos/id/177/300/400',
category: 'beauty',
tags: ['抗衰老', '紧致', '提亮'],
hotLevel: 5
},
{
id: 9,
name: 'Zara复古风衣',
price: 299,
sales: 12345,
image: 'https://picsum.photos/id/111/300/400',
category: 'fashion',
tags: ['复古', '百搭', '时尚'],
hotLevel: 4
},
{
id: 10,
name: 'Uniqlo羊绒衫',
price: 199,
sales: 23456,
image: 'https://picsum.photos/id/222/300/400',
category: 'fashion',
tags: ['保暖', '舒适', '百搭'],
hotLevel: 5
},
{
id: 11,
name: '星巴克咖啡豆',
price: 88,
sales: 34567,
image: 'https://picsum.photos/id/431/300/400',
category: 'food',
tags: ['香浓', '精品', '中烘'],
hotLevel: 5
},
{
id: 12,
name: '三只松鼠零食礼盒',
price: 158,
sales: 19876,
image: 'https://picsum.photos/id/326/300/400',
category: 'food',
tags: ['零食', '礼盒', '美味'],
hotLevel: 4
}
];
// 模拟分页加载
setTimeout(() => {
const start = (this.data.page - 1) * this.data.pageSize;
const end = start + this.data.pageSize;
const newProducts = allMockProducts.slice(start, end);
this.setData({
products: [...this.data.products, ...newProducts],
hasMore: end < allMockProducts.length,
loading: false,
page: this.data.page + 1
});
}, 800);
// 接口失败或无有效数据时,停止加载,保持现有列表
this.setData({
loading: false,
hasMore: false
});
},
// 切换商品选中状态
@@ -293,6 +265,76 @@ Page({
this.loadProducts();
},
// 跳转到个人中心
goToProfile() {
// 检查是否登录
const token = wx.getStorageSync('token');
if (!token) {
wx.showModal({
title: '未登录',
content: '请先登录后再访问个人中心',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
wx.redirectTo({
url: '/pages/login/login'
});
}
}
});
return;
}
// 已登录,跳转到个人中心页面
wx.navigateTo({
url: '/pages/profile/profile'
});
},
// 头像拖动结束,自动吸附到边缘
onAvatarDragEnd(e: any) {
const { x, y } = e.detail;
const { windowWidth, windowHeight } = this.data;
const avatarSize = 100; // 头像尺寸 100rpx
const edgeMargin = 10; // 边缘边距
// 计算头像中心点位置
const centerX = x + avatarSize / 2;
const centerY = y + avatarSize / 2;
// 判断离哪个边更近(只吸附到左右两侧)
let targetX = x;
if (centerX < windowWidth / 2) {
// 离左边更近,吸附到左侧
targetX = edgeMargin;
} else {
// 离右边更近,吸附到右侧
targetX = windowWidth - avatarSize - edgeMargin;
}
// 限制 Y 坐标在合理范围内
let targetY = y;
const topLimit = 50; // 顶部留出空间
const bottomLimit = windowHeight - 150; // 底部留出按钮空间
if (targetY < topLimit) {
targetY = topLimit;
} else if (targetY > bottomLimit) {
targetY = bottomLimit;
}
// 启用动画并设置新位置
this.setData({
avatarAnimation: true,
avatarX: targetX,
avatarY: targetY
});
// 保存头像位置到本地存储
wx.setStorageSync('avatarPosition', { x: targetX, y: targetY });
},
// 分享功能
onShareAppMessage() {
return {

View File

@@ -3,7 +3,6 @@
<!-- 页面标题 -->
<view class="page-header">
<text class="page-title">选择商品</text>
<text class="page-subtitle">运营提供</text>
</view>
<!-- 商品网格列表 -->
@@ -22,23 +21,15 @@
data-id="{{item.id}}"
data-name="{{item.name}}"
>
<view class="checkbox">
<view class="checkbox-icon {{selectedProduct === item.id ? 'checked' : ''}}"></view>
</view>
<view class="product-image-wrapper">
<image class="product-image" src="{{item.image}}" mode="aspectFill" />
<view class="select-indicator" wx:if="{{selectedProduct === item.id}}">
<view class="check-icon"></view>
</view>
<image class="product-image" src="{{item.image}}" mode="aspectFit" />
</view>
<text class="product-name">{{item.name}}</text>
</view>
</view>
<!-- 加载提示 -->
<view class="loading-more" wx:if="{{hasMore}}">
<text class="loading-text">加载更多...</text>
</view>
<view class="no-more" wx:else>
<text class="no-more-text">没有更多了</text>
</view>
</scroll-view>
<!-- 底部按钮 -->
@@ -46,9 +37,25 @@
<button
class="generate-btn"
bindtap="goToGenerate"
disabled="{{!selectedProduct}}"
>
去生成内容
</button>
</view>
<!-- 可拖动的悬浮头像 -->
<movable-area class="movable-area">
<movable-view
class="avatar-btn"
direction="all"
x="{{avatarX}}"
y="{{avatarY}}"
animation="{{avatarAnimation}}"
damping="50"
friction="5"
bindtap="goToProfile"
bindtouchend="onAvatarDragEnd"
>
<image class="avatar-image" src="{{userAvatar}}" mode="aspectFill" />
</movable-view>
</movable-area>
</view>

View File

@@ -9,13 +9,16 @@ page {
display: flex;
flex-direction: column;
background: white;
width: 100%;
max-width: 100vw;
overflow-x: hidden;
}
/* 页面头部 */
.page-header {
padding: 40rpx 30rpx 30rpx;
background: white;
border-bottom: 1rpx solid #f0f0f0;
text-align: center;
}
.page-title {
@@ -23,95 +26,118 @@ page {
font-size: 40rpx;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 8rpx;
}
.page-subtitle {
display: block;
font-size: 24rpx;
color: #999;
}
/* 商品滚动容器 */
.product-scroll {
flex: 1;
padding: 0 0 110rpx 0;
padding: 0;
background: white;
}
/* 商品网格布局 */
.product-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
padding: 20rpx;
display: flex;
flex-wrap: wrap;
justify-content: space-between; /* 两端对齐 */
padding: 0 12rpx; /* 减小左右padding */
padding-bottom: 200rpx;
box-sizing: border-box;
width: 100%;
}
/* 商品卡片 */
.product-card {
background: white;
border-radius: 12rpx;
overflow: hidden;
border: 2rpx solid #f0f0f0;
transition: all 0.3s;
border: 2rpx solid #E5E5E5;
padding: 0; /* 重置padding使用绝对定位控制布局 */
position: relative;
transition: all 0.2s;
box-sizing: border-box;
width: 356rpx; /* 178px × 2 */
height: 436rpx; /* 218px × 2 */
margin-bottom: 12rpx; /* 减小底部间距 */
}
.product-card.selected {
border-color: #07c160;
box-shadow: 0 4rpx 16rpx rgba(7, 193, 96, 0.2);
border-color: #07C160;
border-width: 4rpx;
}
/* Checkbox 样式 */
.checkbox {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 44rpx;
height: 44rpx;
z-index: 10;
}
.checkbox-icon {
width: 44rpx;
height: 44rpx;
border: 2rpx solid #E5E5E5;
border-radius: 50%;
background: white;
transition: all 0.2s;
}
.checkbox-icon.checked {
background: #07C160;
border-color: #07C160;
position: relative;
}
.checkbox-icon.checked::after {
content: '';
position: absolute;
left: 14rpx;
top: 10rpx;
width: 10rpx;
height: 18rpx;
border: solid white;
border-width: 0 3rpx 3rpx 0;
transform: rotate(45deg);
}
.product-image-wrapper {
width: 100%;
height: 330rpx;
background: #f5f5f5;
position: relative;
width: 212rpx; /* 106px × 2 */
height: 212rpx; /* 106px × 2 */
position: absolute;
top: 50%; /* 垂直居中 */
left: 50%; /* 水平居中 */
transform: translate(-50%, -50%); /* 精确居中 */
overflow: hidden;
}
.product-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* 选中指示器 */
.select-indicator {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 48rpx;
height: 48rpx;
background: #07c160;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.4);
}
.check-icon {
width: 24rpx;
height: 24rpx;
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="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
object-fit: cover; /* 强制填充保持1:1比例 */
aspect-ratio: 1 / 1; /* 强制1:1比例 */
}
.product-name {
display: block;
padding: 20rpx;
font-size: 28rpx;
color: #1a1a1a;
font-weight: 500;
line-height: 1.4;
height: 78rpx;
font-size: 32rpx; /* 16px × 2 */
color: #333;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
text-overflow: clip; /* 不显示省略号,直接截断 */
white-space: nowrap;
line-height: 1.4;
position: absolute;
bottom: 32rpx; /* 距离底部 16px × 2 */
left: 0;
right: 0;
padding: 0 16rpx; /* 左右留边距 */
box-sizing: border-box;
max-width: 100%; /* 确保不超出容器 */
}
/* 加载提示 */
@@ -133,17 +159,19 @@ page {
bottom: 0;
left: 0;
right: 0;
padding: 8rpx 30rpx;
padding: 8rpx 0;
background: white;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.05);
border-top: 1rpx solid #f0f0f0;
padding-bottom: calc(8rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: center;
}
.generate-btn {
width: 100%;
width: 90% !important;
min-width: 90% !important;
height: 80rpx;
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
color: white;
@@ -153,6 +181,7 @@ page {
font-weight: 500;
box-shadow: 0 2rpx 8rpx rgba(7, 193, 96, 0.25);
transition: all 0.3s;
margin: 0 !important;
}
.generate-btn::after {
@@ -168,3 +197,41 @@ page {
color: #999;
box-shadow: none;
}
/* 可拖动区域 */
.movable-area {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none; /* 不阻塞其他点击事件 */
z-index: 100;
}
/* 悬浮头像按钮 */
.avatar-btn {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: white;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: 4rpx solid #f0f0f0;
pointer-events: auto; /* 开启头像本身的点击 */
opacity: 1; /* 默认显示 */
}
.avatar-btn:active {
transform: scale(0.95);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
.avatar-image {
width: 100%;
height: 100%;
border-radius: 50%;
}

View File

@@ -1,5 +1,5 @@
{
"navigationBarTitleText": "万花筒AI助手",
"navigationBarBackgroundColor": "#07c160",
"navigationBarTextStyle": "white"
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}

View File

@@ -12,12 +12,52 @@ Page({
// 检查是否已登录检查token
const token = wx.getStorageSync('token');
if (token) {
wx.switchTab({
wx.redirectTo({
url: '/pages/home/home'
});
}
},
// 返回上一页
goBack() {
// 使用 reLaunch 跳转到首页,确保页面栈清空
wx.reLaunch({
url: '/pages/home/home'
});
},
// 显示更多菜单
showMore() {
wx.showActionSheet({
itemList: ['关于我们', '联系客服'],
success: (res) => {
if (res.tapIndex === 0) {
// 关于我们
wx.showToast({
title: '功能开发中',
icon: 'none'
});
} else if (res.tapIndex === 1) {
// 联系客服
wx.showToast({
title: '功能开发中',
icon: 'none'
});
}
}
});
},
// 显示帮助
showHelp() {
wx.showModal({
title: '帮助',
content: '如果遇到问题,请联系客服获取帮助。',
showCancel: false,
confirmText: '知道了'
});
},
// 同意协议
onAgreeChange(e: any) {
this.setData({
@@ -39,6 +79,28 @@ Page({
});
},
// 跳转到手机号登录页
goToPhoneLogin() {
wx.navigateTo({
url: '/pages/login/phone-login'
});
},
// 登录成功后跳转到首页
loginSuccessToHome() {
wx.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
});
setTimeout(() => {
wx.reLaunch({
url: '/pages/home/home'
});
}, 1500);
},
// 未同意协议时点击登录
handleAgreeFirst() {
if (this.data.loginLoading) return
@@ -108,6 +170,8 @@ Page({
}, false);
if (response.code === 200 && response.data) {
console.log('[微信登录] 后端返回成功:', response.data);
// 保存token
wx.setStorageSync('token', response.data.token);
@@ -116,18 +180,106 @@ Page({
wx.setStorageSync('employeeInfo', response.data.employee);
wx.setStorageSync('username', response.data.employee.name);
}
wx.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
});
setTimeout(() => {
wx.switchTab({
url: '/pages/home/home'
// 登录成功后立即获取并缓存绑定状态
try {
console.log('[微信登录] 开始获取用户信息...');
// 使用EmployeeService获取用户信息
wx.request({
url: `${API.baseURL}/api/employee/profile`,
method: 'GET',
header: {
'Authorization': `Bearer ${response.data.token}`,
'Content-Type': 'application/json'
},
success: (profileResponse) => {
const profileData = profileResponse.data as any;
console.log('[微信登录] 获取用户信息响应:', profileData);
if (profileData.code === 200 && profileData.data) {
const userInfo = profileData.data;
const isBound = userInfo.is_bound_xhs === 1;
const hasCookie = userInfo.has_xhs_cookie === true;
console.log('[微信登录] 绑定状态:', { isBound, hasCookie });
// 更新本地绑定状态缓存
const bindings = wx.getStorageSync('socialBindings') || {};
if (isBound) {
bindings.xiaohongshu = {
phone: userInfo.xhs_phone,
xhs_account: userInfo.xhs_account,
bindTime: userInfo.bound_at || new Date().getTime(),
cookieExpired: !hasCookie
};
} else {
delete bindings.xiaohongshu;
}
wx.setStorageSync('socialBindings', bindings);
console.log('[登录] 已缓存绑定状态:', bindings);
// 检查是否需要跳转到绑定页面
if (!isBound) {
console.log('[微信登录] 未绑定,跳转绑定页');
// 新用户,未绑定小红书,跳转到绑定页面
this.setData({ loginLoading: false }); // 重置加载状态
wx.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
});
setTimeout(() => {
wx.showModal({
title: '欢迎使用',
content: '首次登录需要绑定小红书账号,即将跳转到绑定页面',
showCancel: false,
confirmText: '去绑定',
success: (res) => {
if (res.confirm) {
wx.navigateTo({
url: '/pages/profile/platform-bind/platform-bind'
});
}
}
});
}, 1500);
} else {
console.log('[微信登录] 已绑定,跳转首页');
// 老用户,已绑定,跳转到首页
this.setData({ loginLoading: false }); // 重置加载状态
wx.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
});
setTimeout(() => {
wx.reLaunch({
url: '/pages/home/home'
});
}, 1500);
}
} else {
console.error('[微信登录] 获取绑定状态失败,默认跳转首页');
// 获取绑定状态失败,默认跳转首页
this.loginSuccessToHome();
}
},
fail: (profileError) => {
console.error('[登录] 获取绑定状态失败:', profileError);
// 获取失败,默认跳转首页
this.loginSuccessToHome();
}
});
}, 1500);
} catch (profileError) {
console.error('[登录] 获取绑定状态异常:', profileError);
// 异常情况,默认跳转首页
this.loginSuccessToHome();
}
} else {
throw new Error(response.message || '登录失败');
}

View File

@@ -1,58 +1,65 @@
<!--pages/login/login.wxml-->
<view class="login-container">
<!-- 顶部Logo区域 -->
<view class="logo-section">
<view class="logo-box">
<view class="logo-icon"></view>
<view class="container">
<!-- 登录内容 -->
<view class="login-content">
<!-- Logo区域 -->
<view class="logo-container">
<image class="logo-image" src="/images/logo.png" mode="aspectFit"></image>
</view>
<text class="app-name">AI文章审核平台</text>
<text class="app-slogan">智能内容生成与审核管理</text>
</view>
<!-- 标题区域 -->
<text class="app-title">万花筒</text>
<text class="app-subtitle">快速生成种草内容</text>
<!-- 登录区域 -->
<view class="login-section">
<view class="welcome-text">
<text class="welcome-title">欢迎使用</text>
<text class="welcome-subtitle">请使用微信授权登录</text>
<view class="login-buttons">
<!-- 微信登录按钮(未同意协议) -->
<button
wx:if="{{!agreed}}"
class="wechat-btn {{loginLoading ? 'loading' : ''}}"
bindtap="handleAgreeFirst"
loading="{{loginLoading}}"
hover-class="btn-hover"
>
<image class="wechat-icon" src="/images/wechat-icon.svg" mode="aspectFit"></image>
<text>微信一键登录</text>
</button>
<!-- 微信登录按钮(已同意协议) -->
<button
wx:else
class="wechat-btn {{loginLoading ? 'loading' : ''}}"
open-type="getPhoneNumber"
bindgetphonenumber="handleWechatLogin"
loading="{{loginLoading}}"
hover-class="btn-hover"
>
<image class="wechat-icon" src="/images/wechat-icon.svg" mode="aspectFit"></image>
<text>微信一键登录</text>
</button>
<button
class="phone-btn"
bindtap="goToPhoneLogin"
hover-class="btn-hover"
>
<image class="phone-icon" src="/images/phone-icon.svg" mode="aspectFit"></image>
<text>手机号登录</text>
</button>
</view>
<!-- 微信登录按钮(未同意协议) -->
<button
wx:if="{{!agreed}}"
class="login-btn {{loginLoading ? 'loading' : ''}}"
bindtap="handleAgreeFirst"
loading="{{loginLoading}}"
hover-class="btn-hover"
>
<text class="btn-icon wechat-icon"></text>
<text class="btn-text">微信登录</text>
</button>
<!-- 微信登录按钮(已同意协议,获取手机号) -->
<button
wx:else
class="login-btn {{loginLoading ? 'loading' : ''}}"
open-type="getPhoneNumber"
bindgetphonenumber="handleWechatLogin"
loading="{{loginLoading}}"
hover-class="btn-hover"
>
<text class="btn-icon wechat-icon"></text>
<text class="btn-text">微信登录</text>
</button>
<!-- 协议提示 -->
<view class="agreement-section">
<checkbox-group bindchange="onAgreeChange">
<label class="agreement-label">
<checkbox value="agree" checked="{{agreed}}" color="#ff2442" />
<checkbox value="agree" checked="{{agreed}}" color="#07c160" />
<view class="agreement-text">
<text class="normal-text">已阅读并同意</text>
<text class="link-text" catchtap="goToUserAgreement">《用户协议》</text>
<text class="normal-text">和</text>
<text class="link-text" catchtap="goToPrivacyPolicy">《隐私政策》</text>
<text class="normal-text">已阅读并同意《万花筒AI用户协议》和《隐私协议》允许平台统一管理本人账号信息</text>
</view>
</label>
</checkbox-group>
</view>
</view>
<!-- 底部指示器 -->
<view class="bottom-indicator"></view>
</view>

View File

@@ -1,214 +1,145 @@
/* pages/login/login.wxss */
page {
height: 100vh;
background: #07c160;
overflow: hidden;
background: #ffffff;
}
.container {
width: 100%;
margin: 0 auto;
background-color: #fff;
min-height: 100vh;
position: relative;
}
page::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: rotate 30s linear infinite;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.login-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background: transparent;
position: relative;
z-index: 1;
padding-bottom: 40rpx;
}
/* 顶部Logo区域 */
.logo-section {
.login-content {
flex: 1;
width: 100%;
padding: 200rpx 0 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 60rpx;
}
.logo-box {
width: 160rpx;
height: 160rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 32rpx;
/* Logo容器 */
.logo-container {
width: 200rpx;
height: 200rpx;
background: #fff;
border-radius: 40rpx;
border: 1rpx solid #E5E5E5;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10rpx);
animation: float 3s ease-in-out infinite;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10rpx);
}
.logo-image {
width: 160rpx;
height: 160rpx;
}
.logo-icon {
width: 80rpx;
height: 80rpx;
background: white;
border-radius: 16rpx;
}
.app-name {
.app-title {
font-size: 48rpx;
font-weight: bold;
color: white;
margin-bottom: 16rpx;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
}
.app-slogan {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 1rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
/* 登录区域 - 毛玻璃效果 */
.login-section {
background: rgba(255, 255, 255, 0.85);
border-radius: 40rpx 40rpx 0 0;
padding: 60rpx 40rpx 80rpx;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.15);
backdrop-filter: blur(40rpx);
-webkit-backdrop-filter: blur(40rpx);
position: relative;
overflow: hidden;
border: 1rpx solid rgba(255, 255, 255, 0.3);
}
.login-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2rpx;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent);
}
.welcome-text {
margin-bottom: 60rpx;
}
.welcome-title {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 12rpx;
}
.welcome-subtitle {
display: block;
font-size: 28rpx;
color: rgba(0, 0, 0, 0.5);
}
.login-btn {
width: 80%;
max-width: 600rpx;
background: linear-gradient(135deg, #07c160 0%, #2dd573 100%);
color: white;
border: none;
border-radius: 48rpx;
padding: 36rpx;
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.app-subtitle {
font-size: 28rpx;
color: #999;
margin-bottom: 120rpx;
}
.login-buttons {
width: 100%;
padding: 0 5%;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 32rpx;
}
.wechat-btn {
width: 100% !important;
min-width: 100% !important;
margin-left: 0 !important;
margin-right: 0 !important;
padding: 28rpx;
font-size: 34rpx;
font-weight: 500;
color: #fff;
background: #07C160;
border: none;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.25);
transition: all 0.3s;
margin: 0 auto 32rpx;
gap: 16rpx;
box-sizing: border-box;
}
.login-btn::after {
.wechat-btn::after {
border: none;
}
.btn-hover {
opacity: 0.9;
transform: translateY(2rpx);
box-shadow: 0 4rpx 16rpx rgba(7, 193, 96, 0.2);
}
.login-btn.loading {
.wechat-btn.loading {
opacity: 0.7;
pointer-events: none;
}
.btn-icon {
margin-right: 12rpx;
position: relative;
z-index: 1;
}
.wechat-icon {
width: 44rpx;
height: 44rpx;
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTcuNSAxMi41QzExLjE1IDEyLjUgNiAxNy4xIDYgMjIuOEM2IDI2LjEgNy42NSAyOSAxMC4yIDMxTDkgMzUuNUwxMy41IDMzLjJDMTQuOCAzMy43IDE2LjEgMzQgMTcuNSAzNEMyMy44NSAzNCAyOSAyOS40IDI5IDIzLjdDMjkgMTguMSAyMy44NSAxMi41IDE3LjUgMTIuNVpNMTQuNSAyNkMxMy4xMiAyNiAxMiAyNC44OCAxMiAyMy41QzEyIDIyLjEyIDEzLjEyIDIxIDE0LjUgMjFDMTUuODggMjEgMTcgMjIuMTIgMTcgMjMuNUMxNyAyNC44OCAxNS44OCAyNiAxNC41IDI2Wk0yMC41IDI2QzE5LjEyIDI2IDE4IDI0Ljg4IDE4IDIzLjVDMTggMjIuMTIgMTkuMTIgMjEgMjAuNSAyMUMyMS44OCAyMSAyMyAyMi4xMiAyMyAyMy41QzIzIDI0Ljg4IDIxLjg4IDI2IDIwLjUgMjZaTTMxLjUgMjJDMzEuMiAyMiAzMC45IDIyIDMwLjYgMjIuMUMzMS41IDE5LjkgMzEuOCAxNy41IDMxLjMgMTUuMkMzNy4yIDE2LjYgNDEuNSAyMS4yIDQxLjUgMjYuN0M0MS41IDI5LjMgNDAuMiAzMS43IDM4LjEgMzMuNEwzOSAzNy41TDM1LjEgMzUuNkMzNCAxNi4wNSAzMi4yNSAzNi40IDMxLjUgMzYuNEMyNi4xNSAzNi40IDIxLjcgMzIuNSAyMS43IDI3LjdDMjEuNyAyNC40IDI0IDIxLjYgMjcuNSAyMC4xQzI4LjcgMjAuOSAzMC4xIDIxLjQgMzEuNSAyMS40VjIyWk0yNy41IDMwQzI2LjEyIDMwIDI1IDI4Ljg4IDI1IDI3LjVDMjUgMjYuMTIgMjYuMTIgMjUgMjcuNSAyNUMyOC44OCAyNSAzMCAyNi4xMiAzMCAyNy41QzMwIDI4Ljg4IDI4Ljg4IDMwIDI3LjUgMzBaTTM1LjUgMzBDMzQuMTIgMzAgMzMgMjguODggMzMgMjcuNUMzMyAyNi4xMiAzNC4xMiAyNSAzNS41IDI1QzM2Ljg4IDI1IDM4IDI2LjEyIDM4IDI3LjVDMzggMjguODggMzYuODggMzAgMzUuNSAzMFoiIGZpbGw9IndoaXRlIi8+PC9zdmc+');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
width: 48rpx;
height: 40rpx;
}
.btn-text {
position: relative;
z-index: 1;
}
.login-tip {
margin-top: 40rpx;
font-size: 24rpx;
text-align: center;
color: #999;
line-height: 1.6;
}
.tip-text {
color: #999;
}
.tip-link {
color: #07c160;
.phone-btn {
width: 100% !important;
min-width: 100% !important;
margin-left: 0 !important;
margin-right: 0 !important;
padding: 28rpx;
font-size: 34rpx;
font-weight: 500;
color: #333;
background: #fff;
border: 2rpx solid #E5E5E5;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
box-sizing: border-box;
}
/* 协议同意区域 */
.phone-btn::after {
border: none;
}
.phone-icon {
width: 36rpx;
height: 36rpx;
}
.btn-hover {
opacity: 0.8;
}
/* 协议区域 */
.agreement-section {
margin-top: 32rpx;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding-left: 40rpx;
}
.agreement-label {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
}
@@ -219,18 +150,53 @@ checkbox {
flex-shrink: 0;
}
/* 将 checkbox 改为圆形 */
checkbox .wx-checkbox-input {
border-radius: 50% !important;
width: 36rpx !important;
height: 36rpx !important;
}
checkbox .wx-checkbox-input-checked {
background-color: #07c160 !important;
border-color: #07c160 !important;
}
checkbox .wx-checkbox-input-checked::before {
border-radius: 50%;
width: 36rpx;
height: 36rpx;
line-height: 36rpx;
text-align: center;
font-size: 24rpx;
color: #fff;
background: transparent;
transform: translate(-50%, -50%) scale(1);
}
.agreement-text {
flex: 1;
font-size: 24rpx;
line-height: 1.6;
color: rgba(0, 0, 0, 0.6);
color: #999;
}
.normal-text {
color: rgba(0, 0, 0, 0.6);
color: #999;
}
.link-text {
color: #07c160;
font-weight: 500;
}
.bottom-indicator {
position: absolute;
bottom: 16rpx;
left: 50%;
transform: translateX(-50%);
width: 268rpx;
height: 10rpx;
background: #000;
border-radius: 6rpx;
}

View File

@@ -0,0 +1,5 @@
{
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}

View File

@@ -0,0 +1,556 @@
// pages/login/phone-login.ts
import { API } from '../../config/api';
Page({
data: {
phone: '',
code: '',
password: '',
codePlaceholder: '请输入验证码',
codeButtonText: '获取验证码',
canGetCode: false,
canLogin: false,
countdown: 0,
countdownTimer: null as any,
showSuccess: false,
showLoading: false,
agreed: false,
countryCodes: ['+86', '+852', '+853', '+886', '+1', '+44', '+81', '+82'],
countryCodeIndex: 0,
loginType: 'code' // 'code' 或 'password'
},
onLoad() {
console.log('手机号登录页面加载');
},
onUnload() {
if (this.data.countdownTimer) {
clearInterval(this.data.countdownTimer);
}
},
// 区号选择
onCountryCodeChange(e: any) {
this.setData({
countryCodeIndex: e.detail.value
});
},
// 同意协议
onAgreeChange(e: any) {
this.setData({
agreed: e.detail.value.length > 0
});
},
// 跳转到用户协议
goToUserAgreement() {
wx.navigateTo({
url: '/pages/agreement/user-agreement/user-agreement'
});
},
// 跳转到隐私政策
goToPrivacyPolicy() {
wx.navigateTo({
url: '/pages/agreement/privacy-policy/privacy-policy'
});
},
// 返回上一页
goBack() {
wx.navigateBack({
delta: 1,
fail: () => {
wx.navigateTo({
url: '/pages/login/login'
});
}
});
},
// 显示更多菜单
showMore() {
wx.showActionSheet({
itemList: ['关于我们', '联系客服'],
success: (res) => {
if (res.tapIndex === 0) {
wx.showToast({ title: '功能开发中', icon: 'none' });
}
}
});
},
// 显示帮助
showHelp() {
wx.showToast({
title: '功能开发中',
icon: 'none'
});
},
// 手机号输入
onPhoneInput(e: any) {
const phone = e.detail.value;
this.setData({
phone: phone,
canGetCode: phone.length === 11
});
this.checkCanLogin();
},
// 验证码输入
onCodeInput(e: any) {
const code = e.detail.value;
this.setData({
code: code
});
this.checkCanLogin();
},
// 密码输入
onPasswordInput(e: any) {
const password = e.detail.value;
this.setData({
password: password
});
this.checkCanLogin();
},
// 切换登录方式
switchLoginType() {
const newType = this.data.loginType === 'code' ? 'password' : 'code';
this.setData({
loginType: newType,
code: '',
password: ''
});
},
// 检查是否可以登录
checkCanLogin() {
const { phone, code, password, loginType } = this.data;
if (loginType === 'code') {
this.setData({
canLogin: phone.length === 11 && code.length >= 4
});
} else {
this.setData({
canLogin: phone.length === 11 && password.length >= 6
});
}
},
// 获取验证码
getVerifyCode() {
if (!this.data.agreed) {
wx.showToast({
title: '请先同意用户协议',
icon: 'none',
duration: 2000
});
return;
}
if (!this.data.canGetCode || this.data.countdown > 0) {
return;
}
const { phone } = this.data;
if (phone.length !== 11) {
wx.showToast({
title: '请输入正确的手机号',
icon: 'none',
duration: 2000
});
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);
wx.showToast({
title: '网络错误,请稍后重试',
icon: 'none',
duration: 2000
});
}
});
},
// 开始倒计时
startCountdown() {
let countdown = 180;
this.setData({
countdown: countdown,
codeButtonText: `${countdown}秒后重新获取`,
canGetCode: false
});
const timer = setInterval(() => {
countdown--;
if (countdown <= 0) {
clearInterval(timer);
this.setData({
countdown: 0,
codeButtonText: '获取验证码',
canGetCode: this.data.phone.length === 11,
countdownTimer: null
});
} else {
this.setData({
countdown: countdown,
codeButtonText: `${countdown}秒后重新获取`
});
}
}, 1000);
this.setData({
countdownTimer: timer
});
},
// 处理登录
handleLogin() {
if (!this.data.agreed) {
wx.showToast({
title: '请先同意用户协议',
icon: 'none',
duration: 2000
});
return;
}
if (!this.data.canLogin) {
return;
}
const { phone, code, password, loginType } = this.data;
// 显示加载提示
wx.showLoading({
title: '登录中...',
mask: true
});
// 调用后端API进行登录验证
if (loginType === 'code') {
// 验证码登录
wx.request({
url: `${API.baseURL}/api/login/xhs-phone-code`,
method: 'POST',
data: {
phone: phone,
code: code
},
success: async (res: any) => {
wx.hideLoading();
if (res.statusCode === 200 && res.data.code === 200) {
// 登录成功
const { token, employee } = res.data.data;
// 保存token和用户信息
wx.setStorageSync('token', token);
wx.setStorageSync('userInfo', employee);
wx.setStorageSync('employeeInfo', employee);
wx.setStorageSync('username', employee.name);
// 登录成功后立即获取并缓存绑定状态
try {
wx.request({
url: `${API.baseURL}/api/employee/profile`,
method: 'GET',
header: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
success: (profileResponse: any) => {
const profileData = profileResponse.data;
if (profileData.code === 200 && profileData.data) {
const userInfo = profileData.data;
const isBound = userInfo.is_bound_xhs === 1;
const hasCookie = userInfo.has_xhs_cookie === true;
// 更新本地绑定状态缓存
const bindings = wx.getStorageSync('socialBindings') || {};
if (isBound) {
bindings.xiaohongshu = {
phone: userInfo.xhs_phone,
xhs_account: userInfo.xhs_account,
bindTime: userInfo.bound_at || new Date().getTime(),
cookieExpired: !hasCookie
};
} else {
delete bindings.xiaohongshu;
}
wx.setStorageSync('socialBindings', bindings);
console.log('[验证码登录] 已缓存绑定状态:', bindings);
// 检查是否需要跳转到绑定页面
if (!isBound) {
// 新用户,未绑定小红书,跳转到绑定页面
this.setData({
showSuccess: true,
loginLoading: false
});
setTimeout(() => {
this.setData({ showSuccess: false });
wx.showModal({
title: '欢迎使用',
content: '首次登录需要绑定小红书账号,即将跳转到绑定页面',
showCancel: false,
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) {
wx.navigateTo({
url: '/pages/profile/platform-bind/platform-bind'
});
}
}
});
}, 1500);
} else {
// 老用户,已绑定,跳转到首页
this.setData({
showSuccess: true,
loginLoading: false
});
setTimeout(() => {
this.setData({ showSuccess: false });
wx.reLaunch({
url: '/pages/home/home'
});
}, 1500);
}
} else {
// 获取绑定状态失败,默认跳转首页
this.loginSuccessToHome();
}
},
fail: (profileError) => {
console.error('[验证码登录] 获取绑定状态失败:', profileError);
// 获取失败,默认跳转首页
this.loginSuccessToHome();
}
});
} catch (profileError) {
console.error('[验证码登录] 获取绑定状态异常:', profileError);
// 异常情况,默认跳转首页
this.loginSuccessToHome();
}
} else {
// 登录失败
wx.showToast({
title: res.data.message || '登录失败',
icon: 'none',
duration: 2000
});
}
},
fail: (err) => {
wx.hideLoading();
this.setData({ loginLoading: false });
console.error('验证码登录请求失败:', err);
wx.showToast({
title: '网络错误,请稍后重试',
icon: 'none',
duration: 2000
});
}
});
} else {
// 密码登录
wx.request({
url: `${API.baseURL}/api/login/phone-password`,
method: 'POST',
data: {
phone: phone,
password: password
},
success: async (res: any) => {
wx.hideLoading();
if (res.statusCode === 200 && res.data.code === 200) {
// 登录成功
const { token, employee } = res.data.data;
// 保存token和用户信息
wx.setStorageSync('token', token);
wx.setStorageSync('userInfo', employee);
wx.setStorageSync('employeeInfo', employee);
wx.setStorageSync('username', employee.name);
// 登录成功后立即获取并缓存绑定状态
try {
wx.request({
url: `${API.baseURL}/api/employee/profile`,
method: 'GET',
header: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
success: (profileResponse: any) => {
const profileData = profileResponse.data;
if (profileData.code === 200 && profileData.data) {
const userInfo = profileData.data;
const isBound = userInfo.is_bound_xhs === 1;
const hasCookie = userInfo.has_xhs_cookie === true;
// 更新本地绑定状态缓存
const bindings = wx.getStorageSync('socialBindings') || {};
if (isBound) {
bindings.xiaohongshu = {
phone: userInfo.xhs_phone,
xhs_account: userInfo.xhs_account,
bindTime: userInfo.bound_at || new Date().getTime(),
cookieExpired: !hasCookie
};
} else {
delete bindings.xiaohongshu;
}
wx.setStorageSync('socialBindings', bindings);
console.log('[密码登录] 已缓存绑定状态:', bindings);
// 检查是否需要跳转到绑定页面
if (!isBound) {
// 新用户,未绑定小红书,跳转到绑定页面
this.setData({
showSuccess: true,
loginLoading: false
});
setTimeout(() => {
this.setData({ showSuccess: false });
wx.showModal({
title: '欢迎使用',
content: '首次登录需要绑定小红书账号,即将跳转到绑定页面',
showCancel: false,
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) {
wx.navigateTo({
url: '/pages/profile/platform-bind/platform-bind'
});
}
}
});
}, 1500);
} else {
// 老用户,已绑定,跳转到首页
this.setData({
showSuccess: true,
loginLoading: false
});
setTimeout(() => {
this.setData({ showSuccess: false });
wx.reLaunch({
url: '/pages/home/home'
});
}, 1500);
}
} else {
// 获取绑定状态失败,默认跳转首页
this.loginSuccessToHome();
}
},
fail: (profileError) => {
console.error('[密码登录] 获取绑定状态失败:', profileError);
// 获取失败,默认跳转首页
this.loginSuccessToHome();
}
});
} catch (profileError) {
console.error('[密码登录] 获取绑定状态异常:', profileError);
// 异常情况,默认跳转首页
this.loginSuccessToHome();
}
} else {
// 登录失败
wx.showToast({
title: res.data.message || '登录失败',
icon: 'none',
duration: 2000
});
}
},
fail: (err) => {
wx.hideLoading();
this.setData({ loginLoading: false });
console.error('登录请求失败:', err);
wx.showToast({
title: '网络错误,请稍后重试',
icon: 'none',
duration: 2000
});
}
});
}
},
// 登录成功后跳转首页(备用方法)
loginSuccessToHome() {
this.setData({
showSuccess: true,
loginLoading: false
});
setTimeout(() => {
this.setData({ showSuccess: false });
wx.reLaunch({
url: '/pages/home/home'
});
}, 1500);
}
});

View File

@@ -0,0 +1,101 @@
<!--pages/login/phone-login.wxml-->
<view class="container">
<!-- 登录表单 -->
<view class="login-form">
<text class="page-title">登录万花筒</text>
<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>
<!-- 验证码登录 -->
<view class="input-row" wx:if="{{loginType === 'code'}}">
<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="input-row" wx:if="{{loginType === 'password'}}">
<text class="label">密码</text>
<input
type="password"
placeholder="请输入密码"
bindinput="onPasswordInput"
value="{{password}}"
/>
</view>
<!-- 切换登录方式 -->
<view class="switch-login-type" bindtap="switchLoginType">
<text wx:if="{{loginType === 'code'}}">密码登录</text>
<text wx:else>验证码登录</text>
</view>
<!-- 协议提示 -->
<view class="agreement-section">
<checkbox-group bindchange="onAgreeChange">
<label class="agreement-label">
<checkbox value="agree" checked="{{agreed}}" color="#07c160" />
<view class="agreement-text">
<text class="normal-text">我已阅读并同意</text>
<text class="link-text" catchtap="goToUserAgreement">《用户协议》</text>
<text class="normal-text">和</text>
<text class="link-text" catchtap="goToPrivacyPolicy">《隐私政策》</text>
</view>
</label>
</checkbox-group>
</view>
</view>
<!-- 登录按钮独立在login-form外 -->
<view class="button-wrapper">
<button
class="green-btn"
bindtap="handleLogin"
>
登录
</button>
</view>
<!-- 底部指示器 -->
<view class="bottom-indicator"></view>
</view>
<!-- Toast提示 -->
<view class="toast-overlay" wx:if="{{showSuccess}}">
<view class="toast">
<view class="toast-icon">
<view class="success-checkmark"></view>
</view>
<text class="toast-text">登录成功</text>
</view>
</view>
<view class="toast-overlay" wx:if="{{showLoading}}">
<view class="toast">
<view class="toast-loading"></view>
<text class="toast-text">获取验证中</text>
</view>
</view>

View File

@@ -0,0 +1,244 @@
/* pages/login/phone-login.wxss */
page {
height: 100vh;
background: #ffffff;
}
.container {
width: 100%;
margin: 0 auto;
background-color: #fff;
min-height: 100vh;
position: relative;
}
.login-form {
padding: 40rpx 48rpx;
}
.page-title {
font-size: 44rpx;
font-weight: 600;
color: #333;
text-align: center;
display: block;
margin-bottom: 80rpx;
}
.input-row {
display: flex;
align-items: center;
padding: 32rpx 0;
border-bottom: 2rpx solid #E5E5E5;
margin-bottom: 0;
}
.input-row .label {
width: 112rpx;
font-size: 30rpx;
color: #333;
flex-shrink: 0;
}
.input-row .prefix {
font-size: 32rpx;
color: #333;
margin-right: 16rpx;
padding: 4rpx 8rpx;
border-radius: 4rpx;
}
.input-row input {
flex: 1;
border: none;
font-size: 32rpx;
outline: none;
background: transparent;
}
.input-row input::placeholder {
color: #C0C0C0;
}
.input-row .get-code {
font-size: 28rpx !important;
font-weight: normal !important;
color: #666 !important;
padding: 12rpx 24rpx !important;
border: 2rpx solid #E5E5E5 !important;
border-radius: 8rpx !important;
background: #fff !important;
white-space: nowrap;
line-height: 1 !important;
min-width: auto !important;
width: auto !important;
margin: 0 !important;
}
.input-row .get-code::after {
border: none;
}
.input-row .get-code.disabled {
color: #999;
}
/* 切换登录方式 */
.switch-login-type {
text-align: right;
padding: 24rpx 0;
font-size: 28rpx;
color: #07C160;
}
/* 协议区域 */
.agreement-section {
margin-top: 60rpx;
width: 100%;
}
.agreement-label {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
}
checkbox {
transform: scale(0.7);
margin: 0;
padding: 0;
flex-shrink: 0;
}
.agreement-text {
flex: 1;
font-size: 24rpx;
line-height: 1.6;
color: #999;
}
.normal-text {
color: #999;
}
.link-text {
color: #07c160;
font-weight: 500;
}
/* 登录按钮容器 */
.button-wrapper {
width: 90%;
margin: 24rpx auto 0;
padding: 0;
}
.button-wrapper .green-btn {
background: #07C160;
color: #fff;
border: none;
border-radius: 16rpx;
padding: 32rpx 0;
font-size: 34rpx;
font-weight: 500;
width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
transition: opacity 0.2s;
text-align: center;
}
.button-wrapper .green-btn::after {
border: none;
}
.button-wrapper .green-btn:active {
opacity: 0.8;
}
.bottom-indicator {
position: absolute;
bottom: 16rpx;
left: 50%;
transform: translateX(-50%);
width: 268rpx;
height: 10rpx;
background: #000;
border-radius: 6rpx;
}
/* Toast 提示 */
.toast-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
pointer-events: none;
}
.toast {
background: rgba(76, 76, 76, 0.9);
border-radius: 24rpx;
padding: 48rpx 64rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
min-width: 272rpx;
}
.toast-icon {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.success-checkmark {
width: 72rpx;
height: 72rpx;
border: 6rpx solid #fff;
border-radius: 50%;
position: relative;
}
.success-checkmark::after {
content: '';
position: absolute;
left: 20rpx;
top: 16rpx;
width: 16rpx;
height: 32rpx;
border: solid #fff;
border-width: 0 6rpx 6rpx 0;
transform: rotate(45deg);
}
.toast-text {
color: #fff;
font-size: 28rpx;
text-align: center;
line-height: 1.4;
}
.toast-loading {
width: 72rpx;
height: 72rpx;
border: 6rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@@ -1,7 +1,7 @@
<!--pages/profile/about/about.wxml-->
<view class="page-container">
<view class="logo-section">
<view class="app-logo"></view>
<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 File

@@ -26,38 +26,11 @@ page {
.app-logo {
width: 160rpx;
height: 160rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
backdrop-filter: blur(10rpx);
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
position: relative;
margin-bottom: 30rpx;
}
.app-logo::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80rpx;
height: 80rpx;
background: white;
border-radius: 50%;
}
.app-logo::after {
content: '';
position: absolute;
width: 32rpx;
height: 32rpx;
background: #07c160;
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.app-name {
font-size: 40rpx;
font-weight: bold;

View File

@@ -1,14 +1,13 @@
// pages/profile/article-detail/article-detail.ts
import { EmployeeService } from '../../../services/employee';
import { getImageUrl } from '../../../utils/util';
Page({
data: {
articleId: 0,
productName: '',
article: {
title: '',
content: '',
topic: '',
publishLink: '', // 小红书链接
images: [] as Array<{
id: number;
@@ -33,36 +32,23 @@ Page({
async loadArticleDetail() {
try {
// 调用专门的详情API
const response = await EmployeeService.getPublishRecordDetail(this.data.articleId);
console.log('=== 详情响应 ===', response);
console.log('response.data:', response.data);
if (response.code === 200 && response.data) {
console.log('product_name:', response.data.product_name);
console.log('title:', response.data.title);
console.log('content:', response.data.content);
console.log('images:', response.data.images);
console.log('tags:', response.data.tags);
console.log('topic:', response.data.topic);
this.setData({
productName: response.data.product_name || '未知商品',
article: {
title: response.data.title || '',
content: response.data.content || '',
topic: response.data.topic || '',
publishLink: response.data.publish_link || '', // 获取小红书链接
images: response.data.images || [], // 获取图片列表
tags: response.data.tags || [] // 获取标签列表
publishLink: response.data.publish_link || '',
images: (response.data.images || []).map((img: any) => ({
...img,
image_url: getImageUrl(img.image_url),
image_thumb_url: getImageUrl(img.image_thumb_url || img.image_url)
})),
tags: response.data.tags || []
},
loading: false
});
console.log('=== 设置后的数据 ===');
console.log('productName:', this.data.productName);
console.log('article:', this.data.article);
} else {
throw new Error(response.message || '加载失败');
}
@@ -81,8 +67,8 @@ Page({
}
},
// 分享链接(自动复制)
shareArticle() {
// 复制小红书链接
copyLink() {
const publishLink = this.data.article.publishLink;
if (!publishLink) {
@@ -93,7 +79,6 @@ Page({
return;
}
// 复制到剪贴板
wx.setClipboardData({
data: publishLink,
success: () => {
@@ -110,5 +95,16 @@ Page({
});
}
});
},
// 预览图片
previewImage(e: any) {
const { url } = e.currentTarget.dataset;
const urls = this.data.article.images.map((img: any) => img.image_url);
wx.previewImage({
current: url,
urls: urls
});
}
});

View File

@@ -1,65 +1,48 @@
<!--pages/profile/article-detail/article-detail.wxml-->
<view class="page-container">
<!-- 商品信息卡片 -->
<view class="product-card">
<text class="card-title">选中商品</text>
<text class="product-name">{{productName}}</text>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<text class="loading-text">加载中...</text>
</view>
<!-- 生成的文章 -->
<!-- 文案内容 -->
<scroll-view class="article-container" scroll-y enable-flex wx:if="{{!loading}}">
<view class="article-wrapper">
<view class="article-header">
<text class="article-title" user-select>{{article.title}}</text>
</view>
<view class="article-content" wx:if="{{article.content}}">
<text class="content-text" user-select>{{article.content}}</text>
</view>
<!-- 文章图片 -->
<!-- 文章图片画廊 -->
<view class="article-images" wx:if="{{article.images && article.images.length > 0}}">
<view
class="image-item"
wx:for="{{article.images}}"
wx:key="id"
>
<image
class="article-image"
src="{{item.image_thumb_url || item.image_url}}"
mode="aspectFill"
lazy-load
/>
<view class="image-gallery">
<view
class="image-item"
wx:for="{{article.images}}"
wx:key="id">
<image
class="gallery-image"
src="{{item.image_url}}"
mode="aspectFill"
bindtap="previewImage"
data-url="{{item.image_url}}"
/>
</view>
</view>
</view>
<!-- 文章标签 -->
<view class="article-tags" wx:if="{{article.tags && article.tags.length > 0}}">
<text
class="tag-item"
wx:for="{{article.tags}}"
wx:key="index"
user-select
>
#{{item}}
</text>
<view class="article-header">
<!-- 标题(只读) -->
<text class="article-title" user-select>{{article.title}}</text>
</view>
<!-- Topic标签如果tags为空则显示topic -->
<view class="article-tags" wx:elif="{{article.topic}}">
<text class="tag-item" user-select>#{{article.topic}}</text>
<view class="article-content">
<!-- 内容(只读) -->
<text class="content-text" user-select>{{article.content}}</text>
</view>
<!-- 小红书链接 -->
<view class="xhs-link-section" wx:if="{{article.publishLink}}">
<view class="link-label">
<text class="iconfont icon-link link-icon"></text>
<text class="link-text">小红书链接</text>
<view class="link-header">
<text class="link-label">小红书链接</text>
<button class="copy-btn" bindtap="copyLink">
<text class="copy-text">复制链接</text>
</button>
</view>
<view class="link-value">
<text user-select>{{article.publishLink}}</text>
@@ -67,12 +50,4 @@
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="action-bar" wx:if="{{!loading}}">
<button class="share-btn" bindtap="shareArticle">
<view class="btn-icon icon-share"></view>
<text class="btn-text">分享链接</text>
</button>
</view>
</view>

View File

@@ -2,43 +2,164 @@
page {
background: #f8f8f8;
height: 100%;
width: 100%;
overflow-x: hidden; /* 防止横向滚动 */
}
.page-container {
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
padding-bottom: 120rpx; /* 为底部操作栏留出空间 */
overflow-x: hidden; /* 防止横向滚动 */
}
/* 商品卡片 */
.product-card {
background: linear-gradient(135deg, #ff2442 0%, #ff6b8b 100%);
border-radius: 16rpx;
/* 文案容器 */
.article-container {
flex: 1;
background: white;
overflow-y: auto;
overflow-x: hidden;
}
.article-wrapper {
padding: 30rpx;
margin: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(255, 36, 66, 0.2);
box-sizing: border-box;
width: calc(100% - 40rpx); /* 确保不超出屏幕 */
padding-bottom: 100rpx;
}
.card-title {
display: block;
/* 文章图片画廊 - 水平滚动 */
.article-images {
margin-bottom: 32rpx;
}
.image-gallery {
display: flex;
gap: 16rpx;
overflow-x: auto;
padding: 16rpx 0 24rpx;
-webkit-overflow-scrolling: touch;
}
.image-gallery::-webkit-scrollbar {
display: none;
}
.image-item {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
overflow: hidden;
flex-shrink: 0;
background: #f5f5f5;
transition: transform 0.2s;
}
.image-item:active {
transform: scale(0.98);
}
.gallery-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 标签样式 */
.article-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
padding-bottom: 20rpx;
margin-bottom: 24rpx;
}
.tag-item {
padding: 8rpx 20rpx;
background: linear-gradient(135deg, #e6f7ed 0%, #d0f0de 100%);
border-radius: 20rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 12rpx;
color: #07c160;
font-weight: 500;
}
.product-name {
display: block;
font-size: 32rpx;
color: white;
.article-header {
margin-bottom: 24rpx;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
/* 标题(只读) */
.article-title {
font-size: 36rpx;
color: #1a1a1a;
font-weight: bold;
line-height: 1.4;
display: block;
}
.article-content {
margin-bottom: 24rpx;
position: relative;
}
/* 内容文本(只读) */
.content-text {
font-size: 28rpx;
color: #333;
line-height: 1.8;
display: block;
white-space: pre-wrap;
word-break: break-word;
}
/* 小红书链接区域 */
.xhs-link-section {
margin-top: 40rpx;
padding: 24rpx;
background: #f8f9fa;
border-radius: 16rpx;
border: 1rpx solid #e8e8e8;
}
.link-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.link-label {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.copy-btn {
padding: 8rpx 20rpx;
height: auto;
line-height: 1;
background: linear-gradient(135deg, #07c160 0%, #0ae97a 100%);
border-radius: 20rpx;
border: none;
font-size: 24rpx;
color: white;
}
.copy-btn::after {
border: none;
}
.copy-text {
color: white;
font-size: 24rpx;
}
.link-value {
font-size: 26rpx;
color: #666;
word-break: break-all;
line-height: 1.6;
padding: 12rpx 16rpx;
background: white;
border-radius: 8rpx;
}
/* 加载状态 */
@@ -53,202 +174,3 @@ page {
font-size: 28rpx;
color: #999;
}
/* 文章容器 */
.article-container {
flex: 1;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
margin: 0 20rpx 20rpx 20rpx;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
width: calc(100% - 40rpx); /* 确保不超出屏幕 */
}
.article-wrapper {
padding: 30rpx;
box-sizing: border-box;
width: 100%;
overflow-x: hidden; /* 防止内容溢出 */
}
.article-header {
margin-bottom: 24rpx;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
box-sizing: border-box;
width: 100%;
}
.article-title {
font-size: 36rpx;
color: #1a1a1a;
font-weight: bold;
line-height: 1.4;
word-wrap: break-word; /* 允许换行 */
word-break: break-word; /* 允许换行 */
}
.article-content {
margin-bottom: 24rpx;
box-sizing: border-box;
width: 100%;
}
.content-text {
font-size: 28rpx;
color: #333;
line-height: 1.8;
white-space: pre-line;
word-wrap: break-word; /* 允许换行 */
word-break: break-word; /* 允许换行 */
}
/* 文章图片 */
.article-images {
margin: 24rpx 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
box-sizing: border-box;
width: 100%;
}
.image-item {
width: 100%;
padding-bottom: 100%; /* 1:1 比例 */
position: relative;
border-radius: 8rpx;
overflow: hidden;
background: #f5f5f5;
}
.article-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* 标签 */
.article-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
padding-bottom: 20rpx;
margin-bottom: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
box-sizing: border-box;
width: 100%;
}
.tag-item {
padding: 4rpx 16rpx;
background: linear-gradient(135deg, #fff5f7 0%, #ffe8ec 100%);
border-radius: 16rpx;
font-size: 24rpx;
color: #ff2442;
font-weight: 500;
line-height: 1.5;
word-wrap: break-word; /* 允许换行 */
}
/* 小红书链接区域 */
.xhs-link-section {
background: #f8f9fa;
border-radius: 12rpx;
padding: 24rpx;
margin-top: 24rpx;
box-sizing: border-box;
width: 100%;
overflow-x: hidden; /* 防止链接超出 */
}
.link-label {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 12rpx;
box-sizing: border-box;
}
.link-icon {
font-size: 28rpx;
flex-shrink: 0; /* 防止图标被压缩 */
color: #ff2442;
}
.link-text {
font-size: 26rpx;
color: #666;
font-weight: 500;
flex-shrink: 0; /* 防止文字被压缩 */
}
.link-value {
font-size: 24rpx;
color: #ff2442;
word-break: break-all; /* 强制换行 */
line-height: 1.6;
overflow-wrap: break-word; /* 允许换行 */
max-width: 100%; /* 确保不超出 */
box-sizing: border-box;
}
/* 底部操作栏 */
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 20rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 999; /* 提高层级 */
}
.share-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #ff2442 0%, #ff6b8b 100%);
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
border: none;
box-shadow: 0 6rpx 20rpx rgba(255, 36, 66, 0.3);
transition: all 0.3s;
}
.share-btn::after {
border: none;
}
.share-btn:active {
transform: scale(0.98);
opacity: 0.9;
}
.btn-icon {
width: 36rpx;
height: 36rpx;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.icon-share {
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE4IDhDMTkuNjU2OSA4IDIxIDYuNjU2ODUgMjEgNUMyMSAzLjM0MzE1IDE5LjY1NjkgMiAxOCAyQzE2LjM0MzEgMiAxNSAzLjM0MzE1IDE1IDVDMTUgNS4xMjk3MSAxNS4wMDk1IDUuMjU3MjMgMTUuMDI3NyA1LjM4MTk3TDguODg1OTMgOC43ODc0NUM4LjQzMzQ1IDguMjg5NTkgNy43OTQzNSA4IDcuMDggOCA1LjQyMzE1IDggNCAxMC42NTY5IDQgMTJDNCAxMi44NjkgNC4zNzcwNiAxMy42NDU5IDQuOTY5NTggMTQuMTk5OEw0Ljk3MTIzIDE0LjE5ODdDNC45ODA3NCAxNC4yMDQ1IDQuOTkwNDkgMTQuMjEgNSAxNC4yMTU0QzUuMzQ5MDUgMTQuNDk5MyA1Ljc1MzggMTQuNzE4NSA2LjE4OTggMTQuODUyNkM2LjQ0ODcxIDE0Ljk0NTYgNi43MjQzOCAxNSA3LjA4IDE1QzcuNzk0MzUgMTUgOC40MzM0NSAxNC43MTA0IDguODg1OTMgMTQuMjEyNUwxNS4wMjc3IDE3LjYxOEMxNS4wMDk1IDE3Ljc0MjggMTUgMTcuODcwMyAxNSAxOUMxNSAyMC42NTY5IDE2LjM0MzEgMjIgMTggMjJDMTkuNjU2OSAyMiAyMSAyMC42NTY5IDIxIDE5QzIxIDE3LjM0MzEgMTkuNjU2OSAxNiAxOCAxNkMxNy4yNDkyIDE2IDE2LjU3NTYgMTYuMjk5IDE2LjA5MjggMTYuNzg2OUw5Ljk3MTIzIDE0LjE5ODdDOS45ODA3NCAxNC4yMDQ1IDkuOTkwNDkgMTQuMjEgMTAgMTQuMjE1NEMxMC4zNDkgMTQuNDk5MyAxMC43NTM4IDE0LjcxODUgMTEuMTg5OCAxNC44NTI2QzExLjQ0ODcgMTQuOTQ1NiAxMS43MjQ0IDE1IDEyLjA4IDE1QzEyLjc5NDQgMTUgMTMuNDMzNCAxNC43MTA0IDEzLjg4NTkgMTQuMjEyNUwxOS4wMjc3IDE3LjYxOEMxOS4wMDk1IDE3Ljc0MjggMTkgMTcuODcwMyAxOSAxOUMxOSAyMC42NTY5IDIwLjM0MzEgMjIgMjIgMjJDMjMuNjU2OSAyMiAyNSAyMC42NTY5IDI1IDE5QzI1IDE3LjM0MzEgMjMuNjU2OSAxNiAyMiAxNkMyMS4yNDkyIDE2IDIwLjU3NTYgMTYuMjk5IDIwLjA5MjggMTYuNzg2OUwxNC45NzEyIDE0LjE5ODdDMTQuOTgwNyAxNC4yMDQ1IDE0Ljk5MDUgMTQuMjEgMTUgMTQuMjE1NFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
}
.btn-text {
font-size: 32rpx;
color: white;
font-weight: 600;
}

View File

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

View File

@@ -1,14 +1,23 @@
// pages/profile/feedback/feedback.ts
import { request } from '../../../utils/request';
Page({
data: {
typeList: ['功能建议', 'Bug反馈', '体验问题', '其他'],
typeIndex: 0,
content: '',
contact: ''
contact: '',
nickname: ''
},
onLoad() {
// 获取用户信息
const employeeInfo = wx.getStorageSync('employeeInfo');
if (employeeInfo && employeeInfo.name) {
this.setData({
nickname: employeeInfo.name
});
}
},
onTypeChange(e: any) {
@@ -18,6 +27,7 @@ Page({
},
onContentInput(e: any) {
console.log('输入内容:', e.detail.value);
this.setData({
content: e.detail.value
});
@@ -29,7 +39,11 @@ Page({
});
},
handleSubmit() {
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: '请输入问题描述',
@@ -43,16 +57,41 @@ Page({
mask: true
});
setTimeout(() => {
wx.hideLoading();
wx.showToast({
title: '提交成功',
icon: 'success'
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
}
});
setTimeout(() => {
wx.navigateBack();
}, 1500);
}, 1000);
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

@@ -17,8 +17,7 @@
class="feedback-textarea"
placeholder="请详细描述您遇到的问题或建议"
maxlength="500"
value="{{content}}"
bindInput="onContentInput"
model:value="{{content}}"
/>
<view class="char-count">{{content.length}}/500</view>
</view>
@@ -28,8 +27,7 @@
<input
class="feedback-input"
placeholder="请输入您的手机号或邮箱"
value="{{contact}}"
bindInput="onContactInput"
model:value="{{contact}}"
/>
</view>
</view>

View File

@@ -65,9 +65,11 @@ page {
.feedback-input {
width: 100%;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #333;
padding: 20rpx;
padding: 0 20rpx;
background: #f8f8f8;
border-radius: 8rpx;
box-sizing: border-box;
@@ -83,11 +85,11 @@ page {
background: #07c160;
color: white;
border: none;
border-radius: 16rpx;
border-radius: 50rpx;
padding: 32rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(255, 36, 66, 0.3);
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
}
.submit-btn::after {

View File

@@ -1,5 +1,5 @@
{
"navigationBarTitleText": "绑定账号",
"navigationBarBackgroundColor": "#ff2442",
"navigationBarTextStyle": "white"
"navigationBarTitleText": "绑定小红书账号",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}

View File

@@ -1,71 +1,41 @@
// pages/profile/platform-bind/platform-bind.ts
import { EmployeeService } from '../../../services/employee';
Page({
data: {
platformType: '',
platformName: '',
countryCode: '+86',
phone: '',
code: '',
codeSending: false,
codeButtonText: '获取验证码',
countdown: 0,
binded: false,
bindTime: ''
countdownTimer: null as any,
showSuccess: false,
showLoading: false,
showFail: false,
loadingText: '加载中...',
countryCodes: ['+86', '+852', '+853', '+886', '+1', '+44', '+81', '+82'],
countryCodeIndex: 0,
pollTimer: null as any, // 轮询定时器
pollCount: 0 // 轮询次数
},
onLoad(options: any) {
const platform = options.platform || 'xiaohongshu';
// 如果是小红书,跳转到专门的小红书登录页
if (platform === 'xiaohongshu') {
wx.redirectTo({
url: '/pages/profile/xhs-login/xhs-login'
});
return;
}
const platformNames: Record<string, string> = {
xiaohongshu: '小红书',
weibo: '微博',
douyin: '抖音'
};
onLoad() {
console.log('小红书绑定页面加载');
},
onUnload() {
if (this.data.countdownTimer) {
clearInterval(this.data.countdownTimer);
}
// 清理轮询定时器
if (this.data.pollTimer) {
clearInterval(this.data.pollTimer);
}
},
// 区号选择
onCountryCodeChange(e: any) {
this.setData({
platformType: platform,
platformName: platformNames[platform]
});
this.loadBindingStatus(platform);
},
// 加载绑定状态
loadBindingStatus(platform: string) {
const bindings = wx.getStorageSync('socialBindings') || {};
const bindingInfo = bindings[platform];
if (bindingInfo) {
const bindTime = new Date(bindingInfo.bindTime);
const formatTime = `${bindTime.getFullYear()}-${String(bindTime.getMonth() + 1).padStart(2, '0')}-${String(bindTime.getDate()).padStart(2, '0')} ${String(bindTime.getHours()).padStart(2, '0')}:${String(bindTime.getMinutes()).padStart(2, '0')}`;
this.setData({
binded: true,
phone: bindingInfo.phone,
bindTime: formatTime
});
}
},
// 选择国家区号
selectCountryCode() {
const codes = ['+86', '+852', '+853', '+886', '+1', '+44', '+81', '+82'];
const names = ['中国大陆', '中国香港', '中国澳门', '中国台湾', '美国', '英国', '日本', '韩国'];
wx.showActionSheet({
itemList: names.map((name, index) => `${name} ${codes[index]}`),
success: (res) => {
this.setData({
countryCode: codes[res.tapIndex]
});
}
countryCodeIndex: e.detail.value
});
},
@@ -83,203 +53,278 @@ Page({
});
},
// 发送验证码
sendCode() {
const phone = this.data.phone;
// 获取验证码
async getVerifyCode() {
if (this.data.countdown > 0) {
return;
}
// 验证手机号
if (!phone || phone.length !== 11) {
const { phone, countryCodes, countryCodeIndex } = this.data;
if (phone.length !== 11) {
wx.showToast({
title: '请输入正确的手机号',
icon: 'none'
icon: 'none',
duration: 2000
});
return;
}
// 手机号格式验证
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(phone)) {
wx.showToast({
title: '手机号格式不正确',
icon: 'none'
});
return;
}
// 设置发送状态
// 显示加载
this.setData({
codeSending: true
showLoading: true
});
// 模拟发送验证码实际应该调用后端API
wx.showLoading({
title: '发送中...',
mask: true
});
setTimeout(() => {
wx.hideLoading();
try {
// 调用后端API发送验证码
const countryCode = countryCodes[countryCodeIndex];
console.log('发送验证码到:', phone, '区号:', countryCode);
await EmployeeService.sendXHSCode(phone);
this.setData({
showLoading: false
});
wx.showToast({
title: '验证码已发送',
icon: 'success'
title: '验证码已发送请在小红书APP中查看',
icon: 'none',
duration: 2000
});
// 开始倒计时
this.startCountdown();
// 在控制台输出模拟验证码(实际开发中删除)
console.log(`验证码: 123456`);
}, 1000);
} catch (error: any) {
this.setData({
showLoading: false
});
wx.showToast({
title: error.message || '发送失败,请重试',
icon: 'none',
duration: 2000
});
}
},
// 倒计时
// 开始倒计时
startCountdown() {
let countdown = 60;
let countdown = 180;
this.setData({
countdown: countdown,
codeSending: true
codeButtonText: `${countdown}秒后重新获取`
});
const timer = setInterval(() => {
countdown--;
if (countdown <= 0) {
clearInterval(timer);
this.setData({
countdown: 0,
codeSending: false
codeButtonText: '获取验证码',
countdownTimer: null
});
} else {
this.setData({
countdown: countdown
countdown: countdown,
codeButtonText: `${countdown}秒后重新获取`
});
}
}, 1000);
this.setData({
countdownTimer: timer
});
},
// 绑定账号
bindAccount() {
const phone = this.data.phone;
const code = this.data.code;
const platform = this.data.platformType;
async bindAccount() {
const { phone, code } = this.data;
// 验证手机号
if (!phone || phone.length !== 11) {
wx.showToast({
title: '请输入正确的手机号',
icon: 'none'
icon: 'none',
duration: 2000
});
return;
}
// 验证验证码
if (!code || code.length !== 6) {
if (!code || code.length < 4) {
wx.showToast({
title: '请输入6位验证码',
icon: 'none'
title: '请输入验证码',
icon: 'none',
duration: 2000
});
return;
}
// 模拟验证码验证实际应该调用后端API
wx.showLoading({
title: '绑定中...',
mask: true
// 显示加载
this.setData({
showLoading: true,
loadingText: '正在验证...',
pollCount: 0
});
setTimeout(() => {
wx.hideLoading();
// 保存绑定信息
const bindings = wx.getStorageSync('socialBindings') || {};
const bindTime = new Date().getTime();
bindings[platform] = {
phone: phone,
bindTime: bindTime
};
wx.setStorageSync('socialBindings', bindings);
// 格式化时间
const time = new Date(bindTime);
const formatTime = `${time.getFullYear()}-${String(time.getMonth() + 1).padStart(2, '0')}-${String(time.getDate()).padStart(2, '0')} ${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
// 更新状态
try {
// 调用后端API进行绑定异步处理
console.log('绑定小红书账号:', { phone, code });
const result = await EmployeeService.bindXHS(phone, code);
// 后端立即返回,开始轮询绑定状态
this.setData({
binded: true,
code: '',
bindTime: formatTime
loadingText: '正在登录小红书...'
});
// 开始轮询绑定状态
this.startPollingBindStatus();
} catch (error: any) {
this.setData({
showLoading: false
});
wx.showToast({
title: '绑定成功',
icon: 'success',
duration: 1500
// 绑定失败
this.setData({
showFail: true
});
}, 1500);
},
// 跳转到用户协议
goToUserAgreement() {
wx.navigateTo({
url: '/pages/agreement/user-agreement/user-agreement'
setTimeout(() => {
this.setData({
showFail: false
});
}, 2000);
}
},
// 开始轮询绑定状态
startPollingBindStatus() {
// 清理之前的定时器
if (this.data.pollTimer) {
clearInterval(this.data.pollTimer);
}
// 立即查询一次
this.checkBindStatus();
// 每1秒查询一次最多轮询60次60秒
const timer = setInterval(() => {
this.checkBindStatus();
}, 1000);
this.setData({
pollTimer: timer
});
},
// 跳转到隐私政策
goToPrivacy() {
wx.navigateTo({
url: '/pages/agreement/privacy-policy/privacy-policy'
});
},
// 跳转到个人信息保护政策
goToPersonalInfo() {
// TODO: 创建个人信息保护政策页面
wx.showToast({
title: '功能开发中',
icon: 'none'
});
},
// 解除绑定
unbindAccount() {
const platform = this.data.platformType;
const platformName = this.data.platformName;
wx.showModal({
title: '解除绑定',
content: `确定要解除${platformName}账号绑定吗?`,
confirmText: '解除',
confirmColor: '#07c160',
success: (res) => {
if (res.confirm) {
// 删除绑定信息
const bindings = wx.getStorageSync('socialBindings') || {};
delete bindings[platform];
wx.setStorageSync('socialBindings', bindings);
// 更新状态
// 检查绑定状态
async checkBindStatus() {
try {
const pollCount = this.data.pollCount + 1;
this.setData({ pollCount });
// 超过60次轮询60秒停止轮询
if (pollCount > 60) {
this.stopPolling();
this.setData({
showLoading: false,
showFail: true
});
setTimeout(() => {
this.setData({
binded: false,
phone: '',
code: '',
bindTime: ''
showFail: false
});
}, 2000);
return;
}
console.log(`查询绑定状态: 第${pollCount}`);
const response = await EmployeeService.getBindXHSStatus();
const status = response.data;
console.log('绑定状态:', status);
if (!status) {
console.error('状态数据为空');
return;
}
if (status.status === 'success') {
// 绑定成功
this.stopPolling();
this.setData({
showLoading: false,
showSuccess: true
});
// 保存绑定信息到本地
const bindings = wx.getStorageSync('socialBindings') || {};
bindings.xiaohongshu = {
phone: this.data.phone,
xhs_account: status.xhs_account || this.data.phone,
bindTime: new Date().getTime(),
cookieExpired: false
};
wx.setStorageSync('socialBindings', bindings);
setTimeout(() => {
this.setData({
showSuccess: false
});
wx.showToast({
title: '已解除绑定',
icon: 'success',
duration: 1500,
success: () => {
setTimeout(() => {
wx.navigateBack();
}, 1500);
}
// 绑定成功后直接跳转到首页
wx.reLaunch({
url: '/pages/home/home'
});
}
}, 1500);
} else if (status.status === 'failed') {
// 绑定失败
this.stopPolling();
this.setData({
showLoading: false,
showFail: true
});
wx.showToast({
title: status.error || '绑定失败',
icon: 'none',
duration: 3000
});
setTimeout(() => {
this.setData({
showFail: false
});
}, 2000);
} else if (status.status === 'processing') {
// 仍在处理中,继续轮询
this.setData({
loadingText: status.message || '正在登录小红书...'
});
}
});
} catch (error: any) {
console.error('查询绑定状态失败:', error);
// 查询失败,继续轮询
}
},
// 停止轮询
stopPolling() {
if (this.data.pollTimer) {
clearInterval(this.data.pollTimer);
this.setData({
pollTimer: null
});
}
}
});

View File

@@ -1,84 +1,79 @@
<!--pages/profile/platform-bind/platform-bind.wxml-->
<view class="page-container">
<view class="platform-header">
<view class="platform-icon {{platformType}}"></view>
<view class="platform-text">
<text class="platform-name">{{platformName}}</text>
<text class="platform-tip">绑定后可同步发布内容</text>
</view>
</view>
<!-- 未绑定状态 -->
<view class="bind-form" wx:if="{{!binded}}">
<!-- 手机号 -->
<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="bindAccount">
确认
</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 class="bind-info" wx:else>
<view class="info-card">
<view class="info-item">
<text class="info-label">绑定手机号</text>
<text class="info-value">{{phone}}</text>
</view>
<view class="info-item">
<text class="info-label">绑定时间</text>
<text class="info-value">{{bindTime}}</text>
<view class="container">
<!-- 绑定内容 -->
<view class="bind-content">
<!-- 小红书Logo -->
<view class="xhs-logo">
<view class="logo-box">
<text class="logo-text">小红书</text>
</view>
</view>
<button class="unbind-btn" bindtap="unbindAccount">
解除绑定
</button>
<text class="page-title">请绑定小红书账号</text>
<text class="page-subtitle">手机号未注册小红书会导致绑定失败</text>
<view class="bind-form">
<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>
<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 class="bottom-indicator"></view>
</view>
<!-- Toast提示 -->
<view class="toast-overlay" wx:if="{{showSuccess}}">
<view class="toast">
<view class="toast-icon">
<view class="success-checkmark"></view>
</view>
<text class="toast-text">绑定成功</text>
</view>
</view>
<view class="toast-overlay" wx:if="{{showLoading}}">
<view class="toast">
<view class="toast-loading"></view>
<text class="toast-text">{{loadingText}}</text>
</view>
</view>
<view class="toast-overlay" wx:if="{{showFail}}">
<view class="toast">
<view class="toast-icon info">
<view class="info-icon">i</view>
</view>
<text class="toast-text">绑定失败,手机\n号未注册账号</text>
</view>
</view>

View File

@@ -1,247 +1,240 @@
/* pages/profile/platform-bind/platform-bind.wxss */
page {
background: #f8f8f8;
height: 100%;
height: 100vh;
background: #ffffff;
}
.page-container {
padding: 30rpx;
.container {
width: 100%;
margin: 0 auto;
background-color: #fff;
min-height: 100vh;
box-sizing: border-box;
position: relative;
}
/* 平台头部 */
.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-icon.weibo {
background: linear-gradient(135deg, #ff8200 0%, #ffb84d 100%);
}
.platform-icon.douyin {
background: linear-gradient(135deg, #000000 0%, #333333 100%);
}
.platform-text {
flex: 1;
.bind-content {
padding: 80rpx 48rpx;
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;
.xhs-logo {
width: 112rpx;
height: 112rpx;
margin-bottom: 48rpx;
}
.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 {
.logo-box {
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;
height: 100%;
background: #FF2442;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.bind-btn::after {
.logo-text {
color: #fff;
font-size: 24rpx;
font-weight: 600;
}
.page-title {
font-size: 44rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
display: block;
}
.page-subtitle {
font-size: 28rpx;
color: #999;
margin-bottom: 80rpx;
display: block;
}
.bind-form {
width: 100%;
}
.input-row {
display: flex;
align-items: center;
padding: 32rpx 0;
border-bottom: 2rpx solid #E5E5E5;
margin-bottom: 0;
}
.input-row .label {
width: 112rpx;
font-size: 30rpx;
color: #333;
flex-shrink: 0;
}
.input-row .prefix {
font-size: 32rpx;
color: #333;
margin-right: 16rpx;
padding: 4rpx 8rpx;
border-radius: 4rpx;
}
.input-row input {
flex: 1;
border: none;
font-size: 32rpx;
outline: none;
background: transparent;
}
.input-row input::placeholder {
color: #C0C0C0;
}
.input-row .get-code {
font-size: 28rpx !important;
font-weight: normal !important;
color: #666 !important;
padding: 12rpx 24rpx !important;
border: 2rpx solid #E5E5E5 !important;
border-radius: 8rpx !important;
background: #fff !important;
white-space: nowrap;
line-height: 1 !important;
min-width: auto !important;
width: auto !important;
margin: 0 !important;
}
.input-row .get-code::after {
border: none;
}
/* 协议提示 */
.agreement-tip {
.input-row .get-code.disabled {
color: #999 !important;
}
.red-btn {
width: 100%;
padding: 28rpx;
font-size: 34rpx;
font-weight: 500;
color: #fff;
background: #FF2442;
border: none;
border-radius: 16rpx;
margin-top: 80rpx;
transition: opacity 0.2s;
}
.red-btn::after {
border: none;
}
.red-btn:active {
opacity: 0.8;
}
.bottom-indicator {
position: absolute;
bottom: 16rpx;
left: 50%;
transform: translateX(-50%);
width: 268rpx;
height: 10rpx;
background: #000;
border-radius: 6rpx;
}
/* Toast 提示 */
.toast-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 4rpx;
padding: 0 8rpx;
align-items: center;
justify-content: center;
z-index: 1000;
pointer-events: none;
}
.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;
}
/* 绑定信息 */
.bind-info {
.toast {
background: rgba(76, 76, 76, 0.9);
border-radius: 24rpx;
padding: 48rpx 64rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.info-card {
background: white;
border-radius: 16rpx;
padding: 40rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
gap: 24rpx;
min-width: 272rpx;
}
.info-item:last-child {
border-bottom: none;
.toast-icon {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
}
.info-label {
font-size: 28rpx;
color: #666;
.success-checkmark {
width: 72rpx;
height: 72rpx;
border: 6rpx solid #fff;
border-radius: 50%;
position: relative;
}
.info-value {
font-size: 28rpx;
color: #1a1a1a;
font-weight: 500;
.success-checkmark::after {
content: '';
position: absolute;
left: 20rpx;
top: 16rpx;
width: 16rpx;
height: 32rpx;
border: solid #fff;
border-width: 0 6rpx 6rpx 0;
transform: rotate(45deg);
}
.unbind-btn {
width: 100%;
height: 96rpx;
background: white;
color: #07c160;
border: 2rpx solid #07c160;
border-radius: 12rpx;
.info-icon {
width: 64rpx;
height: 64rpx;
border: 4rpx solid #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 32rpx;
font-weight: 600;
}
.unbind-btn::after {
border: none;
.toast-text {
color: #fff;
font-size: 28rpx;
text-align: center;
line-height: 1.4;
white-space: pre-line;
}
.toast-loading {
width: 72rpx;
height: 72rpx;
border: 6rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@@ -1,5 +1,7 @@
{
"navigationBarTitleText": "我的",
"navigationBarBackgroundColor": "#07c160",
"navigationBarTextStyle": "white"
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"onReachBottomDistance": 50,
"enablePullDownRefresh": true
}

View File

@@ -1,5 +1,7 @@
// pages/profile/profile.ts
import { EmployeeService } from '../../services/employee';
import { API, isDevOrTrial } from '../../config/api';
import { getImageUrl } from '../../utils/util';
Page({
data: {
@@ -7,11 +9,16 @@ Page({
enterpriseName: '',
avatar: '',
publishedCount: 0,
pendingCount: 0, // 待审核
rejectedCount: 0, // 已驳回
userInfo: null as any,
isBoundXHS: false,
xhsAccount: ''
xhsAccount: '',
publishRecords: [] as any[], // 发布记录列表
currentPage: 1, // 当前页码
pageSize: 10, // 每页数量
hasMore: true, // 是否还有更多数据
loading: false, // 是否正在加载
showLogoutBtn: false, // 是否显示退出登录按钮(开发版和体验版)
showUnbindDialog: false // 是否显示解绑确认弹窗
},
onLoad() {
@@ -24,23 +31,21 @@ Page({
return;
}
// 设置是否显示退出登录按钮(开发版和体验版显示)
this.setData({
showLogoutBtn: isDevOrTrial()
});
// 加载用户信息
this.loadUserInfo();
// 获取数据概览
this.loadStatistics();
// 加载发布记录
this.loadPublishRecords();
},
onShow() {
// 设置 tabBar 选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
active: 2
});
}
// 每次显示页面时刷新数据
this.loadUserInfo();
this.loadStatistics();
this.loadPublishRecords(true); // 重置加载
},
// 加载用户信息
@@ -53,7 +58,7 @@ Page({
this.setData({
username: userInfo.name,
enterpriseName: userInfo.enterprise_name || '',
avatar: userInfo.avatar || '',
avatar: getImageUrl(userInfo.avatar),
userInfo,
isBoundXHS: userInfo.is_bound_xhs === 1,
xhsAccount: userInfo.xhs_account || ''
@@ -69,34 +74,253 @@ Page({
}
},
// 加载数据统计
async loadStatistics() {
// 加载发布记录
async loadPublishRecords(reset: boolean = false) {
// 如果正在加载或没有更多数据,直接返回
if (this.data.loading || (!reset && !this.data.hasMore)) {
return;
}
this.setData({ loading: true });
try {
// 获取已发布数量从发布记录API获取
const publishResponse = await EmployeeService.getMyPublishRecords(1, 1);
if (publishResponse.code === 200 && publishResponse.data) {
const page = reset ? 1 : this.data.currentPage;
const response = await EmployeeService.getMyPublishRecords(page, this.data.pageSize);
if (response.code === 200 && response.data) {
const records = response.data.list || [];
// 提取封面图片(使用第一张图片)
const processedRecords = records.map((record: any) => ({
...record,
cover_image: record.images && record.images.length > 0
? getImageUrl(record.images[0].image_url)
: '/images/default-cover.png'
}));
// 如果是重置,替换数据;否则追加数据
const newRecords = reset ? processedRecords : [...this.data.publishRecords, ...processedRecords];
this.setData({
publishedCount: publishResponse.data.total || 0
publishedCount: response.data.total || 0,
publishRecords: newRecords,
currentPage: page + 1,
hasMore: newRecords.length < (response.data.total || 0),
loading: false
});
}
// TODO: 后端需要添加待审核和已驳回的API
// 目前使用默认值
this.setData({
pendingCount: 0,
rejectedCount: 0
});
} catch (error) {
console.error('加载数据统计失败:', error);
// 使用默认值
console.error('加载发布记录失败:', error);
this.setData({
publishedCount: 0,
pendingCount: 0,
rejectedCount: 0
publishRecords: [],
loading: false,
hasMore: false
});
}
},
// 滚动到底部加载更多
onReachBottom() {
this.loadPublishRecords(false);
},
// 下拉刷新
onPullDownRefresh() {
this.loadUserInfo();
this.loadPublishRecords(true);
setTimeout(() => {
wx.stopPullDownRefresh();
}, 1000);
},
// 选择头像
chooseAvatar() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
await this.uploadAvatar(tempFilePath);
},
fail: (err) => {
console.error('选择图片失败:', err);
if (err.errMsg !== 'chooseMedia:fail cancel') {
wx.showToast({
title: '选择图片失败',
icon: 'none'
});
}
}
});
},
// 上传头像
async uploadAvatar(filePath: string) {
wx.showLoading({ title: '上传中...', mask: true });
try {
// 获取 token
const token = wx.getStorageSync('token');
if (!token) {
throw new Error('未登录');
}
// 上传到 OSS
const uploadResult = await this.uploadToOSS(filePath);
if (!uploadResult.url) {
throw new Error('上传失败');
}
// 更新用户资料
const response = await EmployeeService.updateProfile({
avatar: uploadResult.url
});
wx.hideLoading();
if (response.code === 200) {
wx.showToast({
title: '更换成功',
icon: 'success'
});
// 更新本地头像
this.setData({
avatar: getImageUrl(uploadResult.url)
});
} else {
throw new Error(response.message || '更新失败');
}
} catch (error: any) {
console.error('上传头像失败:', error);
wx.hideLoading();
wx.showToast({
title: error.message || '上传失败',
icon: 'none'
});
}
},
// 上传文件到 OSS
async uploadToOSS(filePath: string): Promise<{ url: string }> {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token');
wx.uploadFile({
url: `${API.baseURL}/api/employee/upload-avatar`,
filePath: filePath,
name: 'file',
header: {
'Authorization': `Bearer ${token}`
},
success: (res) => {
try {
const data = JSON.parse(res.data);
if (data.code === 200 && data.data && data.data.url) {
resolve({ url: data.data.url });
} else {
reject(new Error(data.message || '上传失败'));
}
} catch (err) {
reject(new Error('解析响应失败'));
}
},
fail: (err) => {
reject(err);
}
});
});
},
// 处理小红书账号点击
handleAccountClick() {
if (this.data.isBoundXHS) {
// 已绑定,显示自定义解绑确认弹窗
this.setData({
showUnbindDialog: true
});
} else {
// 未绑定,跳转绑定页
wx.navigateTo({
url: '/pages/profile/platform-bind/platform-bind'
});
}
},
// 隐藏弹窗
hideDialog() {
this.setData({
showUnbindDialog: false
});
},
// 阻止事件冒泡
stopPropagation() {
// 空函数,用于阻止事件冒泡
},
// 确认解绑
async confirmUnbind() {
// 先隐藏弹窗
this.setData({
showUnbindDialog: false
});
// 执行解绑操作
await this.handleUnbind();
},
// 执行解绑操作
async handleUnbind() {
wx.showLoading({ title: '解绑中...', mask: true });
try {
const response = await EmployeeService.unbindXHS();
if (response.code === 200) {
wx.hideLoading();
wx.showToast({
title: '解绑成功',
icon: 'success'
});
// 更新绑定状态
this.setData({
isBoundXHS: false,
xhsAccount: ''
});
} else {
throw new Error(response.message || '解绑失败');
}
} catch (error: any) {
console.error('解绑失败:', error);
wx.hideLoading();
wx.showToast({
title: error.message || '解绑失败',
icon: 'none'
});
}
},
// 跳转到发布记录详情
viewRecordDetail(e: any) {
const { id } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/profile/article-detail/article-detail?id=${id}`
});
},
// 跳转到发布记录详情(旧方法,保留兼容)
goToRecordDetail(e: any) {
const { id } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/profile/article-detail/article-detail?id=${id}`
});
},
// 返回首页
goToHome() {
wx.redirectTo({
@@ -172,12 +396,31 @@ Page({
wx.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
try {
// 调用后端退出登录接口删除Redis中token
const token = wx.getStorageSync('token');
if (token) {
await wx.request({
url: `${API.baseURL}/api/logout`,
method: 'POST',
header: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
}
} catch (error) {
console.error('退出登录接口调用失败:', error);
// 即使接口失败也继续清理本地状态
}
// 清除本地存储
wx.clearStorageSync();
// 返回登录页
wx.redirectTo({
// 使用 reLaunch 跳转到登录页,清空页面栈,防止返回
wx.reLaunch({
url: '/pages/login/login'
});
}

View File

@@ -1,103 +1,70 @@
<!--pages/profile/profile.wxml-->
<view class="page-wrapper">
<!-- 用户信息区域 -->
<view class="header-section">
<view class="user-card">
<view class="avatar">
<view class="profile-content">
<!-- 用户信息 -->
<view class="user-info">
<view class="avatar" bindtap="chooseAvatar">
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill" />
<text wx:else class="avatar-icon"></text>
</view>
<view class="user-info">
<text class="username">{{username}}</text>
<text wx:if="{{enterpriseName}}" class="enterprise-name">{{enterpriseName}}</text>
<view class="user-badge">
<text class="badge-text">内容管理员</text>
<view wx:else class="avatar-placeholder">
<view class="avatar-icon"></view>
</view>
</view>
<view class="setting-btn">
<text class="setting-icon"></text>
<view class="user-details">
<text class="user-name">{{username}}</text>
<text wx:if="{{enterpriseName}}" class="enterprise-name">{{enterpriseName}}</text>
</view>
</view>
<!-- 小红书账号 -->
<view class="account-row" bindtap="handleAccountClick">
<text class="row-label">小红书账号</text>
<view class="row-right">
<text class="row-value">{{isBoundXHS ? xhsAccount : '未绑定'}}</text>
<view class="arrow-icon"></view>
</view>
</view>
<!-- 发布记录 -->
<view class="section-title">发布记录</view>
<!-- 空状态 -->
<view class="empty-records" wx:if="{{publishedCount === 0}}">
<text>暂无记录</text>
</view>
<!-- 记录网格 -->
<view class="records-grid" wx:if="{{publishedCount > 0}}">
<view class="record-card" wx:for="{{publishRecords}}" wx:key="id" bindtap="viewRecordDetail" data-id="{{item.id}}">
<view class="record-image">
<image src="{{item.cover_image}}" mode="aspectFill" />
</view>
<text class="record-title">{{item.title}}</text>
</view>
</view>
<!-- 加载提示 -->
<view class="loading-more" wx:if="{{loading}}">
<text class="loading-text">加载中...</text>
</view>
<view class="no-more" wx:if="{{!hasMore && publishedCount > 0}}">
<text class="no-more-text">没有更多了</text>
</view>
</view>
<!-- 数据概览 -->
<view class="data-overview">
<view class="overview-title">数据概览</view>
<view class="stats-grid">
<view class="stat-item" bindtap="goToPublished">
<text class="stat-value">{{publishedCount}}</text>
<text class="stat-label">已发布</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-value">{{pendingCount}}</text>
<text class="stat-label">待审核</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-value">{{rejectedCount}}</text>
<text class="stat-label">已驳回</text>
</view>
</view>
</view>
<!-- 功能区域 -->
<view class="function-section">
<view class="section-title">常用功能</view>
<view class="function-grid">
<view class="function-item" bindtap="goToStats">
<view class="function-icon icon-chart"></view>
<text class="function-text">数据统计</text>
</view>
<view class="function-item" bindtap="goToMyArticles">
<view class="function-icon icon-article"></view>
<text class="function-text">我的文章</text>
</view>
<view class="function-item" bindtap="goToFavorites">
<view class="function-icon icon-favorite"></view>
<text class="function-text">收藏夹</text>
</view>
<view class="function-item" bindtap="goToNotifications">
<view class="function-icon icon-notification"></view>
<text class="function-text">消息通知</text>
</view>
</view>
</view>
<!-- 设置列表 -->
<view class="setting-section">
<view class="setting-item" bindtap="goToUserInfo">
<view class="item-left">
<text class="item-icon icon-profile"></text>
<text class="item-text">个人资料</text>
</view>
<text class="item-arrow"></text>
</view>
<view class="setting-item" bindtap="goToSocialBinding">
<view class="item-left">
<text class="item-icon icon-link"></text>
<text class="item-text">社交账号绑定</text>
</view>
<text class="item-arrow"></text>
</view>
<view class="setting-item" bindtap="goToFeedback">
<view class="item-left">
<text class="item-icon icon-feedback"></text>
<text class="item-text">意见反馈</text>
</view>
<text class="item-arrow"></text>
</view>
<view class="setting-item" bindtap="goToAbout">
<view class="item-left">
<text class="item-icon icon-about"></text>
<text class="item-text">关于我们</text>
</view>
<text class="item-arrow"></text>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section">
<!-- 底部退出登录按钮(仅开发环境显示) -->
<view class="logout-wrapper" wx:if="{{showLogoutBtn}}">
<button class="logout-btn" bindtap="handleLogout">退出登录</button>
</view>
<!-- 解绑确认半屏弹窗 -->
<view class="dialog-mask" wx:if="{{showUnbindDialog}}" bindtap="hideDialog" catchtouchmove="stopPropagation">
<view class="dialog-container" catchtap="stopPropagation">
<view class="dialog-title">确定解绑</view>
<view class="dialog-content">解绑账号后将无法继续使用它登录小红书账号</view>
<view class="dialog-buttons">
<button class="dialog-btn confirm-btn" bindtap="confirmUnbind">确定解绑</button>
<button class="dialog-btn cancel-btn" bindtap="hideDialog">取消</button>
</view>
</view>
</view>
</view>

View File

@@ -2,271 +2,219 @@
page {
height: 100%;
width: 100vw;
background: #f8f8f8;
background: white;
}
.page-wrapper {
min-height: 100%;
width: 100%;
padding-bottom: 200rpx;
box-sizing: border-box;
background: #f8f8f8;
background: white;
}
/* 用户信息区域 */
.header-section {
width: 100%;
background: #07c160;
padding: 50rpx 30rpx 70rpx;
box-sizing: border-box;
.profile-content {
padding: 32rpx;
padding-top: 48rpx;
padding-bottom: 180rpx; /* 为底部按钮留出空间 */
}
.user-card {
/* 用户信息 */
.user-info {
display: flex;
align-items: center;
position: relative;
gap: 24rpx;
margin-bottom: 48rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: white;
border: 4rpx solid rgba(255, 255, 255, 0.3);
margin-right: 24rpx;
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
position: relative;
cursor: pointer;
}
.avatar:active {
opacity: 0.8;
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 60rpx;
object-fit: cover;
}
.avatar-text {
font-size: 60rpx;
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
}
.username {
font-size: 36rpx;
font-weight: bold;
color: white;
margin-bottom: 8rpx;
}
.enterprise-name {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 8rpx;
}
.user-badge {
display: inline-block;
align-self: flex-start;
}
.badge-text {
font-size: 22rpx;
color: #07c160;
background: white;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-weight: 500;
}
.setting-btn {
width: 60rpx;
height: 60rpx;
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 30rpx;
}
.setting-icon {
font-size: 36rpx;
.avatar-icon {
width: 48rpx;
height: 48rpx;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><circle cx="12" cy="8" r="4" stroke="%23999" stroke-width="2" fill="none"/><path d="M4 20C4 16.6863 7.58172 14 12 14C16.4183 14 20 16.6863 20 20" stroke="%23999" stroke-width="2" stroke-linecap="round" fill="none"/></svg>');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/* 数据概览 */
.data-overview {
width: 100%;
background: white;
margin: 0;
padding: 30rpx;
box-sizing: border-box;
}
.overview-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
}
.stats-grid {
display: flex;
align-items: center;
}
.stat-item {
flex: 1;
.user-details {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.user-name {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.enterprise-name {
font-size: 26rpx;
color: #999;
}
/* 小红书账号 */
.account-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 0;
border-bottom: 2rpx solid #f0f0f0;
margin-bottom: 48rpx;
}
.stat-value {
font-size: 40rpx;
font-weight: bold;
color: #07c160;
margin-bottom: 8rpx;
.row-label {
font-size: 32rpx;
color: #333;
}
.stat-label {
.row-right {
display: flex;
align-items: center;
gap: 16rpx;
}
.row-value {
font-size: 30rpx;
color: #999;
}
.arrow-icon {
width: 16rpx;
height: 28rpx;
background-image: url('data:image/svg+xml;utf8,<svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1L7 7L1 13" stroke="%23C0C0C0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/* 发布记录 */
.section-title {
font-size: 28rpx;
color: #999;
margin-bottom: 32rpx;
}
.empty-records {
display: flex;
align-items: center;
justify-content: center;
height: 600rpx;
color: #999;
font-size: 30rpx;
}
.records-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
}
.record-card {
border-radius: 16rpx;
overflow: hidden;
}
.record-image {
width: 100%;
padding-top: 100%; /* 1:1 比例 */
position: relative;
background: #f5f5f5;
overflow: hidden;
}
.record-image image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.record-title {
font-size: 26rpx;
color: #333;
line-height: 1.4;
margin-top: 16rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 加载提示 */
.loading-more,
.no-more {
text-align: center;
padding: 40rpx 0;
}
.loading-text,
.no-more-text {
font-size: 24rpx;
color: #999;
}
.stat-divider {
width: 1rpx;
height: 60rpx;
background: #f0f0f0;
}
/* 功能区域 */
.function-section {
width: 100%;
background: #f8f8f8;
padding: 20rpx 0;
margin-top: 20rpx;
box-sizing: border-box;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
padding: 0 30rpx;
}
.function-grid {
width: 100%;
/* 退出登录按钮 */
.logout-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16rpx 32rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
background: white;
padding: 30rpx;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
box-sizing: border-box;
}
.function-item {
display: flex;
flex-direction: column;
align-items: center;
transition: transform 0.3s;
}
.function-item:active {
transform: scale(0.95);
}
.function-icon {
width: 88rpx;
height: 88rpx;
background: linear-gradient(135deg, #e6f7ed 0%, #d0f0de 100%);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
margin-bottom: 12rpx;
}
.function-text {
font-size: 22rpx;
color: #666;
text-align: center;
}
/* 设置列表*/
.setting-section {
width: 100%;
background: white;
margin-top: 20rpx;
padding-top: 10rpx;
overflow: hidden;
box-sizing: border-box;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
transition: background 0.2s;
}
.setting-item:last-child {
border-bottom: none;
}
.setting-item:active {
background: #fafafa;
}
.item-left {
display: flex;
align-items: center;
}
.item-icon {
font-size: 36rpx;
margin-right: 16rpx;
}
.item-text {
font-size: 28rpx;
color: #333;
}
.item-arrow {
font-size: 40rpx;
color: #d0d0d0;
font-weight: 300;
}
/* 退出登录 */
.logout-section {
width: 100%;
background: white;
padding: 0;
margin-top: 20rpx;
margin-bottom: 40rpx;
box-sizing: border-box;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.05);
border-top: 1rpx solid #f0f0f0;
}
.logout-btn {
width: 100%;
height: 88rpx;
background: white;
padding: 32rpx;
text-align: center;
border: none;
font-size: 30rpx;
color: #07c160;
font-weight: 600;
border-radius: 0;
color: #ff3b30;
border: 2rpx solid #ff3b30;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.logout-btn::after {
@@ -274,151 +222,103 @@ page {
}
.logout-btn:active {
opacity: 0.8;
background: #fafafa;
background: #fff5f5;
transform: scale(0.98);
}
/* 底部Tab栏 */
.bottom-tabbar {
/* 半屏弹窗样式 */
.dialog-mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
height: 120rpx;
background: white;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: space-around;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
border-top: 1rpx solid #f0f0f0;
z-index: 1000;
padding-bottom: env(safe-area-inset-bottom);
align-items: flex-end;
animation: fadeIn 0.3s ease-out;
}
.tab-item {
flex: 1;
.dialog-container {
width: 100%;
background: white;
border-radius: 32rpx 32rpx 0 0;
padding: 48rpx 32rpx;
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
animation: slideUp 0.3s ease-out;
}
.dialog-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 24rpx;
}
.dialog-content {
font-size: 28rpx;
color: #666;
text-align: center;
line-height: 1.6;
margin-bottom: 48rpx;
}
.dialog-buttons {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.dialog-btn {
width: 100%;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
padding: 12rpx 0;
transition: all 0.3s ease;
border: none;
background: transparent;
}
.tab-item.active .tab-icon {
transform: scale(1.1);
.dialog-btn::after {
border: none;
}
.tab-icon {
font-size: 48rpx;
margin-bottom: 6rpx;
transition: all 0.3s ease;
.cancel-btn {
color: #666;
}
.tab-item.active .tab-icon {
filter: drop-shadow(0 2rpx 8rpx rgba(255, 36, 66, 0.3));
.cancel-btn:active {
opacity: 0.6;
}
.tab-text {
font-size: 22rpx;
color: #999;
font-weight: 500;
transition: all 0.3s ease;
.confirm-btn {
color: #ff3b30;
}
.tab-item.active .tab-text {
color: #07c160;
font-weight: 600;
.confirm-btn:active {
opacity: 0.6;
}
/* 图标样式 */
.avatar-icon {
width: 60rpx;
height: 60rpx;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%2307c160"><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;
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.setting-icon {
width: 36rpx;
height: 36rpx;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.icon-chart,
.icon-article,
.icon-favorite,
.icon-notification,
.icon-profile,
.icon-link,
.icon-feedback,
.icon-about {
width: 40rpx;
height: 40rpx;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.icon-chart {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%2307c160"><path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 16.99z"/></svg>');
}
.icon-article {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%2307c160"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>');
}
.icon-favorite {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%2307c160"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>');
}
.icon-notification {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%2307c160"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>');
}
.icon-profile {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><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>');
}
.icon-link {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>');
}
.icon-feedback {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 9h12v2H6V9zm8 5H6v-2h8v2zm4-6H6V6h12v2z"/></svg>');
}
.icon-about {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23666"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>');
}
.icon-home,
.icon-user {
width: 48rpx;
height: 48rpx;
display: inline-block;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.icon-home {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>');
}
.tab-item.active .icon-home {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%2307c160"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>');
}
.icon-user {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><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>');
}
.tab-item.active .icon-user {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%2307c160"><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>');
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}

View File

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

View File

@@ -1,5 +1,6 @@
// pages/profile/published/published.ts
import { EmployeeService, PublishRecord as ApiPublishRecord } from '../../../services/employee';
import { getImageUrl } from '../../../utils/util';
interface Article {
id: number;
@@ -20,7 +21,8 @@ interface Article {
Page({
data: {
articles: [] as Article[]
articles: [] as Article[],
hasLoaded: false // 是否已加载过数据
},
onLoad() {
@@ -28,7 +30,15 @@ Page({
},
onShow() {
this.loadArticles();
// 仅在未加载过数据时加载
if (!this.data.hasLoaded) {
this.loadArticles();
}
},
// 下拉刷新
onPullDownRefresh() {
this.loadArticles(true);
},
// 初始化模拟数据
@@ -90,7 +100,7 @@ Page({
},
// 加载文章列表
async loadArticles() {
async loadArticles(isPullRefresh: boolean = false) {
try {
// 从后端API获取发布记录
const response = await EmployeeService.getMyPublishRecords(1, 50);
@@ -110,7 +120,11 @@ Page({
title: record.title,
content: '', // 列表页不需要显示完整内容
tags: record.tags || [],
images: record.images || [],
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'
};
@@ -119,14 +133,27 @@ Page({
console.log('转换后的文章列表:', apiArticles);
this.setData({
articles: apiArticles
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失败尝试使用本地数据
@@ -141,8 +168,13 @@ Page({
});
this.setData({
articles: formattedArticles
articles: formattedArticles,
hasLoaded: true
});
if (isPullRefresh) {
wx.stopPullDownRefresh();
}
},
// 格式化时间

View File

@@ -9,7 +9,8 @@ Page({
codeSending: false,
countdown: 0,
agreedToTerms: false,
showAgreementModal: false
showAgreementModal: false,
loginLoading: false // 绑定加载状态,防止重复提交
},
// 选择国家区号
@@ -82,8 +83,12 @@ Page({
});
try {
// 调用后端API发送验证码
const response = await EmployeeService.sendXHSCode(phone);
console.log('开始发送验证码,手机号:', phone);
// 调用后端API发送验证码showLoading=true显示加载提示
const response = await EmployeeService.sendXHSCode(phone, true);
console.log('发送验证码响应:', response);
if (response.code === 200) {
wx.showToast({
@@ -94,7 +99,8 @@ Page({
} else {
wx.showToast({
title: response.message || '发送失败',
icon: 'none'
icon: 'none',
duration: 3000
});
this.setData({
codeSending: false
@@ -102,10 +108,18 @@ Page({
}
} catch (error) {
console.error('发送验证码失败:', error);
// 检查是否是超时错误
const errorMsg = error && typeof error === 'object' && 'message' in error
? (error as any).message
: '发送失败,请重试';
wx.showToast({
title: '发送失败,请重试',
icon: 'none'
title: errorMsg,
icon: 'none',
duration: 3000
});
this.setData({
codeSending: false
});
@@ -114,7 +128,7 @@ Page({
// 倒计时
startCountdown() {
let countdown = 60;
let countdown = 180;
this.setData({
countdown: countdown,
@@ -169,7 +183,18 @@ Page({
return;
}
// 防止重复提交(关键优化)
if (this.data.loginLoading) {
console.log('绑定请求进行中,忽略重复点击');
return;
}
try {
// 设置 loading 状态
this.setData({
loginLoading: true
});
// 调用后端API绑定小红书
const response = await EmployeeService.bindXHS(phone, code);
@@ -191,14 +216,27 @@ Page({
// 延迟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
});
}
},
@@ -239,5 +277,15 @@ Page({
wx.navigateTo({
url: '/pages/agreement/privacy-policy/privacy-policy'
});
},
// 跳转到个人信息保护政策
goToPersonalInfo(e: any) {
e.stopPropagation();
// TODO: 创建个人信息保护政策页面
wx.showToast({
title: '功能开发中',
icon: 'none'
});
}
});

View File

@@ -1,76 +1,66 @@
<!--pages/profile/xhs-login/xhs-login.wxml-->
<view class="page-container">
<!-- 背景动画 -->
<view class="background-gradient"></view>
<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="login-card">
<!-- 平台头部 -->
<view class="platform-header">
<view class="platform-icon"></view>
<view class="platform-text">
<text class="platform-name">小红书账号登录</text>
<text class="platform-tip">使用小红书手机号登录</text>
<!-- 未绑定状态 -->
<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="login-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 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>
<!-- 验证码 -->
<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-row">
<checkbox-group bindchange="onAgreementChange">
<label class="agreement-checkbox">
<checkbox value="agree" checked="{{agreedToTerms}}" color="#07c160"/>
<text class="agreement-text">我已阅读并同意</text>
<text class="agreement-link" bindtap="goToUserAgreement">《用户协议》</text>
<text class="agreement-text"></text>
<text class="agreement-link" bindtap="goToPrivacy">《隐私政策》</text>
</label>
</checkbox-group>
</view>
<!-- 登录按钮 -->
<button class="login-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 File

@@ -1,76 +1,36 @@
/* pages/profile/xhs-login/xhs-login.wxss */
page {
background: #07c160;
background: #f8f8f8;
height: 100%;
width: 100%;
}
.page-container {
padding: 30rpx;
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
position: relative;
}
/* 背景渐变动画 */
.background-gradient {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #07c160;
background-size: 200% 200%;
animation: gradientMove 8s ease infinite;
z-index: 0;
}
@keyframes gradientMove {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* 登录卡片 - 毛玻璃效果 */
.login-card {
width: 100%;
max-width: 680rpx;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
border-radius: 32rpx;
padding: 48rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
position: relative;
z-index: 1;
}
/* 平台头部 */
.platform-header {
background: white;
border-radius: 16rpx;
padding: 40rpx;
margin-bottom: 30rpx;
display: flex;
align-items: center;
gap: 24rpx;
margin-bottom: 48rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.platform-icon {
width: 120rpx;
height: 120rpx;
border-radius: 24rpx;
background: #07c160;
border-radius: 20rpx;
flex-shrink: 0;
box-shadow: 0 4rpx 16rpx rgba(255, 36, 66, 0.3);
}
.platform-icon.xiaohongshu {
background: #07c160;
}
.platform-text {
@@ -92,36 +52,36 @@ page {
}
/* 表单 */
.login-form {
width: 100%;
.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: 96rpx;
height: 88rpx;
background: #f8f8f8;
border-radius: 16rpx;
border-radius: 12rpx;
overflow: hidden;
border: 2rpx solid transparent;
transition: all 0.3s ease;
}
.phone-input-wrapper:focus-within {
border-color: #07c160;
background: white;
}
.country-code {
display: flex;
align-items: center;
gap: 8rpx;
padding: 0 24rpx;
padding: 0 20rpx;
height: 100%;
border-right: 1rpx solid #e0e0e0;
flex-shrink: 0;
@@ -150,17 +110,10 @@ page {
.code-input-wrapper {
display: flex;
align-items: center;
height: 96rpx;
height: 88rpx;
background: #f8f8f8;
border-radius: 16rpx;
border-radius: 12rpx;
overflow: hidden;
border: 2rpx solid transparent;
transition: all 0.3s ease;
}
.code-input-wrapper:focus-within {
border-color: #07c160;
background: white;
}
.code-input {
@@ -180,68 +133,59 @@ page {
color: #07c160;
white-space: nowrap;
flex-shrink: 0;
font-weight: 500;
}
.send-code-text.disabled {
color: #999;
}
/* 协议勾选 */
.agreement-row {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32rpx;
}
.agreement-checkbox {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
gap: 4rpx;
}
.agreement-text {
font-size: 24rpx;
color: #666;
line-height: 1.6;
}
.agreement-link {
font-size: 24rpx;
color: #07c160;
line-height: 1.6;
}
/* 登录按钮 */
.login-btn {
.bind-btn {
width: 100%;
height: 96rpx;
height: 88rpx;
background: #07c160;
color: white;
border: none;
border-radius: 48rpx;
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;
box-shadow: 0 4rpx 16rpx rgba(255, 36, 66, 0.3);
transition: all 0.3s ease;
}
.login-btn:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(255, 36, 66, 0.3);
}
.login-btn::after {
.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;

View File

@@ -1,5 +1,5 @@
// 员工端API服务
import { get, post } from '../utils/request';
import { get, post, put, del } from '../utils/request';
import { API } from '../config/api';
// 员工信息接口
@@ -11,6 +11,8 @@ export interface EmployeeInfo {
enterprise_id: number;
enterprise_name: string;
avatar?: string;
nickname?: string;
email?: string;
is_bound_xhs: number;
xhs_account: string;
xhs_phone: string;
@@ -71,10 +73,10 @@ export class EmployeeService {
/**
* 发送小红书验证码
*/
static async sendXHSCode(xhsPhone: string) {
static async sendXHSCode(xhsPhone: string, showLoading = true) {
return post(API.xhs.sendCode, {
xhs_phone: xhsPhone
});
}, showLoading);
}
/**
@@ -87,11 +89,23 @@ export class EmployeeService {
/**
* 绑定小红书账号
*/
static async bindXHS(xhsPhone: string, code: string) {
static async bindXHS(xhsPhone: string, code: string, showLoading = false) {
return post<{ xhs_account: string }>(API.employee.bindXHS, {
xhs_phone: xhsPhone,
code
});
}, showLoading);
}
/**
* 获取小红书绑定状态
*/
static async getBindXHSStatus() {
return get<{
status: string;
message?: string;
xhs_account?: string;
error?: string;
}>(API.employee.bindXHSStatus);
}
/**
@@ -102,12 +116,26 @@ export class EmployeeService {
}
/**
* 获取产品列表(公开接口,不需要登录
* 更新个人资料(昵称、邮箱、头像
*/
static async getProducts() {
return get<{ list: Product[] }>(API.public.products);
static async updateProfile(data: {
nickname?: string;
email?: string;
avatar?: string;
}) {
return put(API.employee.profile, data);
}
/**
* 获取产品列表(支持分页)
*/
static async getProducts(page: number = 1, pageSize: number = 10) {
return get<{ list: Product[]; has_more: boolean }>(API.public.products, {
page,
page_size: pageSize
});
}
/**
* 获取可领取文案列表
*/
@@ -219,4 +247,102 @@ export class EmployeeService {
status
});
}
/**
* 更新文案内容(标题、正文)
*/
static async updateArticleContent(articleId: number, data: {
title: string;
content: string;
}) {
return put(`/api/employee/article/${articleId}`, data);
}
/**
* 编辑发布记录
*/
static async updatePublishRecord(recordId: number, data: {
title?: string;
content?: string;
images?: Array<{
image_url: string;
image_thumb_url?: string;
sort_order: number;
keywords_name?: string;
}>;
tags?: string[];
}) {
return put(`/api/employee/publish-record/${recordId}`, data);
}
/**
* 重新发布种草内容
*/
static async republishRecord(recordId: number) {
return post(`/api/employee/publish-record/${recordId}/republish`, {});
}
/**
* 添加文案图片
*/
static async addArticleImage(articleId: number, data: {
image_url: string;
image_thumb_url?: string;
keywords_name?: string;
}) {
return post(`/api/employee/article/${articleId}/image`, data);
}
/**
* 删除文案图片
*/
static async deleteArticleImage(imageId: number) {
return del(`/api/employee/article/image/${imageId}`);
}
/**
* 更新文案图片排序
*/
static async updateArticleImagesOrder(articleId: number, imageOrders: Array<{
id: number;
sort_order: number;
}>) {
return put(`/api/employee/article/${articleId}/images/order`, {
image_orders: imageOrders
});
}
/**
* 上传图片
*/
static async uploadImage(filePath: string) {
return new Promise<{
code: number;
message: string;
data: {
image_url: string;
image_thumb_url: string;
};
}>((resolve, reject) => {
const token = wx.getStorageSync('token');
wx.uploadFile({
url: `${API.baseURL}/api/employee/upload/image`,
filePath,
name: 'file',
header: {
'Authorization': `Bearer ${token}`
},
success: (res) => {
try {
const data = JSON.parse(res.data);
resolve(data);
} catch (e) {
reject(new Error('解析响应失败'));
}
},
fail: reject
});
});
}
}

View File

@@ -93,3 +93,27 @@ export const getCoverIcon = (topicTypeId: number) => {
]
return icons[topicTypeId % icons.length]
}
// OSS 图片URL前缀
const OSS_PREFIX = 'https://bxmkb-beijing.oss-cn-beijing.aliyuncs.com/Images/'
// 处理图片URL - 自动补全OSS前缀
export const getImageUrl = (imagePath: string | null | undefined): string => {
// 如果为空,返回默认图片
if (!imagePath) {
return '/images/default-cover.png'
}
// 如果已经是完整的URLhttp或https开头直接返回
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
return imagePath
}
// 如果是本地路径(以/开头),直接返回
if (imagePath.startsWith('/')) {
return imagePath
}
// 其他情况补全OSS前缀
return OSS_PREFIX + imagePath
}