feat: 个人中心支持我的作品、草稿箱、创作者统计
This commit is contained in:
@@ -167,4 +167,28 @@ export default class UserManager {
|
||||
return { daily: 3, used: 0, purchased: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的作品
|
||||
*/
|
||||
async getMyWorks() {
|
||||
if (!this.isLoggedIn) return [];
|
||||
try {
|
||||
return await get('/user/my-works', { userId: this.userId });
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取草稿箱
|
||||
*/
|
||||
async getDrafts() {
|
||||
if (!this.isLoggedIn) return [];
|
||||
try {
|
||||
return await get('/user/drafts', { userId: this.userId });
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
/**
|
||||
* 个人中心场景
|
||||
* 个人中心场景 - 支持创作者功能
|
||||
*/
|
||||
import BaseScene from './BaseScene';
|
||||
|
||||
export default class ProfileScene extends BaseScene {
|
||||
constructor(main, params) {
|
||||
super(main, params);
|
||||
// Tab: 0我的作品 1草稿箱 2收藏 3游玩记录
|
||||
this.currentTab = 0;
|
||||
this.tabs = ['作品', '草稿', '收藏', '记录'];
|
||||
|
||||
// 数据
|
||||
this.myWorks = [];
|
||||
this.drafts = [];
|
||||
this.collections = [];
|
||||
this.progress = [];
|
||||
this.currentTab = 0; // 0: 收藏, 1: 历史
|
||||
|
||||
// 统计
|
||||
this.stats = {
|
||||
works: 0,
|
||||
totalPlays: 0,
|
||||
totalLikes: 0,
|
||||
earnings: 0
|
||||
};
|
||||
|
||||
// 滚动
|
||||
this.scrollY = 0;
|
||||
this.maxScrollY = 0;
|
||||
this.isDragging = false;
|
||||
@@ -23,17 +39,39 @@ export default class ProfileScene extends BaseScene {
|
||||
|
||||
async loadData() {
|
||||
if (this.main.userManager.isLoggedIn) {
|
||||
this.collections = await this.main.userManager.getCollections() || [];
|
||||
this.progress = await this.main.userManager.getProgress() || [];
|
||||
try {
|
||||
this.myWorks = await this.main.userManager.getMyWorks?.() || [];
|
||||
this.drafts = await this.main.userManager.getDrafts?.() || [];
|
||||
this.collections = await this.main.userManager.getCollections() || [];
|
||||
this.progress = await this.main.userManager.getProgress() || [];
|
||||
|
||||
// 计算统计
|
||||
this.stats.works = this.myWorks.length;
|
||||
this.stats.totalPlays = this.myWorks.reduce((sum, w) => sum + (w.play_count || 0), 0);
|
||||
this.stats.totalLikes = this.myWorks.reduce((sum, w) => sum + (w.like_count || 0), 0);
|
||||
this.stats.earnings = this.myWorks.reduce((sum, w) => sum + (w.earnings || 0), 0);
|
||||
} catch (e) {
|
||||
console.error('加载数据失败:', e);
|
||||
}
|
||||
}
|
||||
this.calculateMaxScroll();
|
||||
}
|
||||
|
||||
getCurrentList() {
|
||||
switch (this.currentTab) {
|
||||
case 0: return this.myWorks;
|
||||
case 1: return this.drafts;
|
||||
case 2: return this.collections;
|
||||
case 3: return this.progress;
|
||||
default: return [];
|
||||
}
|
||||
}
|
||||
|
||||
calculateMaxScroll() {
|
||||
const list = this.currentTab === 0 ? this.collections : this.progress;
|
||||
const cardHeight = 90;
|
||||
const gap = 12;
|
||||
const headerHeight = 300;
|
||||
const list = this.getCurrentList();
|
||||
const cardHeight = this.currentTab <= 1 ? 100 : 85;
|
||||
const gap = 10;
|
||||
const headerHeight = 260;
|
||||
const contentHeight = list.length * (cardHeight + gap) + headerHeight;
|
||||
this.maxScrollY = Math.max(0, contentHeight - this.screenHeight + 20);
|
||||
}
|
||||
@@ -47,248 +85,421 @@ export default class ProfileScene extends BaseScene {
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
// 背景渐变
|
||||
this.renderBackground(ctx);
|
||||
this.renderHeader(ctx);
|
||||
this.renderUserCard(ctx);
|
||||
this.renderTabs(ctx);
|
||||
this.renderList(ctx);
|
||||
}
|
||||
|
||||
renderBackground(ctx) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, this.screenHeight);
|
||||
gradient.addColorStop(0, '#0f0c29');
|
||||
gradient.addColorStop(0.5, '#302b63');
|
||||
gradient.addColorStop(1, '#24243e');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
|
||||
|
||||
// 顶部返回
|
||||
this.renderHeader(ctx);
|
||||
|
||||
// 用户信息卡片
|
||||
this.renderUserCard(ctx);
|
||||
|
||||
// Tab切换
|
||||
this.renderTabs(ctx);
|
||||
|
||||
// 列表内容
|
||||
this.renderList(ctx);
|
||||
}
|
||||
|
||||
renderHeader(ctx) {
|
||||
// 顶部渐变遮罩
|
||||
const headerGradient = ctx.createLinearGradient(0, 0, 0, 60);
|
||||
headerGradient.addColorStop(0, 'rgba(0,0,0,0.5)');
|
||||
headerGradient.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = headerGradient;
|
||||
ctx.fillRect(0, 0, this.screenWidth, 60);
|
||||
|
||||
// 返回按钮
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '16px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText('‹ 返回', 15, 35);
|
||||
|
||||
// 标题
|
||||
ctx.textAlign = 'center';
|
||||
ctx.font = 'bold 17px sans-serif';
|
||||
ctx.fillText('个人中心', this.screenWidth / 2, 35);
|
||||
|
||||
// 设置按钮
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
||||
ctx.font = '18px sans-serif';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText('⚙', this.screenWidth - 20, 35);
|
||||
}
|
||||
|
||||
renderUserCard(ctx) {
|
||||
const cardY = 60;
|
||||
const cardHeight = 170;
|
||||
const centerX = this.screenWidth / 2;
|
||||
const cardY = 55;
|
||||
const cardH = 145;
|
||||
const user = this.main.userManager;
|
||||
|
||||
// 卡片背景
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.06)';
|
||||
this.roundRect(ctx, 15, cardY, this.screenWidth - 30, cardHeight, 16);
|
||||
this.roundRect(ctx, 12, cardY, this.screenWidth - 24, cardH, 14);
|
||||
ctx.fill();
|
||||
|
||||
// 头像
|
||||
const avatarSize = 55;
|
||||
const avatarY = cardY + 20;
|
||||
const avatarGradient = ctx.createLinearGradient(centerX - 30, avatarY, centerX + 30, avatarY + avatarSize);
|
||||
const avatarSize = 50;
|
||||
const avatarX = 30;
|
||||
const avatarY = cardY + 18;
|
||||
const avatarGradient = ctx.createLinearGradient(avatarX, avatarY, avatarX + avatarSize, avatarY + avatarSize);
|
||||
avatarGradient.addColorStop(0, '#ff6b6b');
|
||||
avatarGradient.addColorStop(1, '#ffd700');
|
||||
ctx.fillStyle = avatarGradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
|
||||
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// 头像文字
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 22px sans-serif';
|
||||
ctx.font = 'bold 20px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(user.nickname ? user.nickname[0] : '游', centerX, avatarY + avatarSize / 2 + 8);
|
||||
ctx.fillText(user.nickname ? user.nickname[0] : '游', avatarX + avatarSize / 2, avatarY + avatarSize / 2 + 7);
|
||||
|
||||
// 昵称
|
||||
// 昵称和ID
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 15px sans-serif';
|
||||
ctx.fillText(user.nickname || '游客用户', centerX, avatarY + avatarSize + 20);
|
||||
ctx.font = 'bold 16px sans-serif';
|
||||
ctx.fillText(user.nickname || '游客用户', avatarX + avatarSize + 15, avatarY + 22);
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.fillText(`ID: ${user.userId || '未登录'}`, avatarX + avatarSize + 15, avatarY + 42);
|
||||
|
||||
// 创作者标签
|
||||
if (this.myWorks.length > 0) {
|
||||
const tagX = avatarX + avatarSize + 15 + ctx.measureText(`ID: ${user.userId || '未登录'}`).width + 10;
|
||||
ctx.fillStyle = '#a855f7';
|
||||
this.roundRect(ctx, tagX, avatarY + 30, 45, 18, 9);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('创作者', tagX + 22, avatarY + 42);
|
||||
}
|
||||
|
||||
// 分割线
|
||||
const lineY = avatarY + avatarSize + 35;
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
|
||||
const lineY = cardY + 80;
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(30, lineY);
|
||||
ctx.lineTo(this.screenWidth - 30, lineY);
|
||||
ctx.moveTo(25, lineY);
|
||||
ctx.lineTo(this.screenWidth - 25, lineY);
|
||||
ctx.stroke();
|
||||
|
||||
// 统计信息
|
||||
// 统计数据
|
||||
const statsY = lineY + 30;
|
||||
const statWidth = (this.screenWidth - 30) / 3;
|
||||
const statW = (this.screenWidth - 24) / 4;
|
||||
const statsData = [
|
||||
{ num: this.progress.length, label: '游玩' },
|
||||
{ num: this.collections.length, label: '收藏' },
|
||||
{ num: this.progress.filter(p => p.is_completed).length, label: '结局' }
|
||||
{ num: this.stats.works, label: '作品' },
|
||||
{ num: this.stats.totalPlays, label: '播放' },
|
||||
{ num: this.stats.totalLikes, label: '获赞' },
|
||||
{ num: this.stats.earnings.toFixed(1), label: '收益' }
|
||||
];
|
||||
|
||||
statsData.forEach((stat, i) => {
|
||||
const x = 15 + statWidth * i + statWidth / 2;
|
||||
const x = 12 + statW * i + statW / 2;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 18px sans-serif';
|
||||
ctx.fillText(stat.num.toString(), x, statsY);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.fillText(stat.label, x, statsY + 16);
|
||||
ctx.font = 'bold 16px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(this.formatNumber(stat.num), x, statsY);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.45)';
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.fillText(stat.label, x, statsY + 15);
|
||||
});
|
||||
}
|
||||
|
||||
renderTabs(ctx) {
|
||||
const tabY = 245;
|
||||
const tabWidth = (this.screenWidth - 30) / 2;
|
||||
const tabs = ['我的收藏', '游玩记录'];
|
||||
|
||||
tabs.forEach((tab, index) => {
|
||||
const x = 15 + index * tabWidth;
|
||||
const tabY = 210;
|
||||
const tabW = (this.screenWidth - 24) / this.tabs.length;
|
||||
|
||||
this.tabRects = [];
|
||||
this.tabs.forEach((tab, index) => {
|
||||
const x = 12 + index * tabW;
|
||||
const isActive = index === this.currentTab;
|
||||
const centerX = x + tabWidth / 2;
|
||||
const centerX = x + tabW / 2;
|
||||
|
||||
// Tab背景
|
||||
ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.45)';
|
||||
ctx.font = isActive ? 'bold 13px sans-serif' : '13px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(tab, centerX, tabY + 12);
|
||||
|
||||
// 下划线
|
||||
if (isActive) {
|
||||
const tabGradient = ctx.createLinearGradient(x, tabY, x + tabWidth, tabY);
|
||||
tabGradient.addColorStop(0, '#ff6b6b');
|
||||
tabGradient.addColorStop(1, '#ffd700');
|
||||
ctx.fillStyle = tabGradient;
|
||||
this.roundRect(ctx, x + 10, tabY, tabWidth - 20, 32, 16);
|
||||
const lineGradient = ctx.createLinearGradient(centerX - 14, tabY + 20, centerX + 14, tabY + 20);
|
||||
lineGradient.addColorStop(0, '#ff6b6b');
|
||||
lineGradient.addColorStop(1, '#ffd700');
|
||||
ctx.fillStyle = lineGradient;
|
||||
this.roundRect(ctx, centerX - 14, tabY + 18, 28, 3, 1.5);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Tab文字
|
||||
ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.5)';
|
||||
ctx.font = isActive ? 'bold 14px sans-serif' : '14px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(tab, centerX, tabY + 21);
|
||||
this.tabRects.push({ x, y: tabY - 5, width: tabW, height: 30, index });
|
||||
});
|
||||
}
|
||||
|
||||
renderList(ctx) {
|
||||
const list = this.currentTab === 0 ? this.collections : this.progress;
|
||||
const startY = 295;
|
||||
const cardHeight = 90;
|
||||
const cardMargin = 12;
|
||||
const padding = 15;
|
||||
const list = this.getCurrentList();
|
||||
const startY = 250;
|
||||
const cardH = this.currentTab <= 1 ? 100 : 85;
|
||||
const gap = 10;
|
||||
const padding = 12;
|
||||
|
||||
// 裁剪区域
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(0, startY - 10, this.screenWidth, this.screenHeight - startY + 10);
|
||||
ctx.rect(0, startY - 5, this.screenWidth, this.screenHeight - startY + 5);
|
||||
ctx.clip();
|
||||
|
||||
if (list.length === 0) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||||
ctx.font = '14px sans-serif';
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.35)';
|
||||
ctx.font = '13px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(
|
||||
this.currentTab === 0 ? '还没有收藏的故事' : '还没有游玩记录',
|
||||
this.screenWidth / 2,
|
||||
startY + 50
|
||||
);
|
||||
const emptyTexts = ['还没有发布作品,去创作吧', '草稿箱空空如也', '还没有收藏的故事', '还没有游玩记录'];
|
||||
ctx.fillText(emptyTexts[this.currentTab], this.screenWidth / 2, startY + 50);
|
||||
|
||||
// 创作引导按钮
|
||||
if (this.currentTab === 0) {
|
||||
const btnY = startY + 80;
|
||||
const btnGradient = ctx.createLinearGradient(this.screenWidth / 2 - 50, btnY, this.screenWidth / 2 + 50, btnY);
|
||||
btnGradient.addColorStop(0, '#a855f7');
|
||||
btnGradient.addColorStop(1, '#ec4899');
|
||||
ctx.fillStyle = btnGradient;
|
||||
this.roundRect(ctx, this.screenWidth / 2 - 50, btnY, 100, 36, 18);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 13px sans-serif';
|
||||
ctx.fillText('✨ 开始创作', this.screenWidth / 2, btnY + 23);
|
||||
this.createBtnRect = { x: this.screenWidth / 2 - 50, y: btnY, width: 100, height: 36 };
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
list.forEach((item, index) => {
|
||||
const y = startY + index * (cardHeight + cardMargin) - this.scrollY;
|
||||
if (y > -cardHeight && y < this.screenHeight) {
|
||||
this.renderListCard(ctx, item, padding, y, this.screenWidth - padding * 2, cardHeight, index);
|
||||
const y = startY + index * (cardH + gap) - this.scrollY;
|
||||
if (y > startY - cardH && y < this.screenHeight) {
|
||||
if (this.currentTab === 0) {
|
||||
this.renderWorkCard(ctx, item, padding, y, this.screenWidth - padding * 2, cardH, index);
|
||||
} else if (this.currentTab === 1) {
|
||||
this.renderDraftCard(ctx, item, padding, y, this.screenWidth - padding * 2, cardH, index);
|
||||
} else {
|
||||
this.renderSimpleCard(ctx, item, padding, y, this.screenWidth - padding * 2, cardH, index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
renderListCard(ctx, item, x, y, width, height, index) {
|
||||
renderWorkCard(ctx, item, x, y, w, h, index) {
|
||||
// 卡片背景
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.06)';
|
||||
this.roundRect(ctx, x, y, width, height, 12);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.05)';
|
||||
this.roundRect(ctx, x, y, w, h, 12);
|
||||
ctx.fill();
|
||||
|
||||
// 封面
|
||||
const coverSize = 65;
|
||||
const coverColors = [
|
||||
const coverW = 70, coverH = h - 16;
|
||||
const colors = this.getGradientColors(index);
|
||||
const coverGradient = ctx.createLinearGradient(x + 8, y + 8, x + 8 + coverW, y + 8 + coverH);
|
||||
coverGradient.addColorStop(0, colors[0]);
|
||||
coverGradient.addColorStop(1, colors[1]);
|
||||
ctx.fillStyle = coverGradient;
|
||||
this.roundRect(ctx, x + 8, y + 8, coverW, coverH, 10);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.8)';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(item.category || '故事', x + 8 + coverW / 2, y + 8 + coverH / 2 + 3);
|
||||
|
||||
const textX = x + 88;
|
||||
const maxW = w - 100;
|
||||
|
||||
// 标题
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(this.truncateText(ctx, item.title || '未命名', maxW - 60), textX, y + 25);
|
||||
|
||||
// 审核状态标签
|
||||
const statusMap = {
|
||||
0: { text: '草稿', color: '#888888' },
|
||||
1: { text: '审核中', color: '#f59e0b' },
|
||||
2: { text: '已发布', color: '#22c55e' },
|
||||
3: { text: '已下架', color: '#ef4444' },
|
||||
4: { text: '被拒绝', color: '#ef4444' }
|
||||
};
|
||||
const status = statusMap[item.status] || statusMap[0];
|
||||
const statusW = ctx.measureText(status.text).width + 12;
|
||||
ctx.fillStyle = status.color + '33';
|
||||
this.roundRect(ctx, textX + ctx.measureText(this.truncateText(ctx, item.title || '未命名', maxW - 60)).width + 8, y + 12, statusW, 18, 9);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = status.color;
|
||||
ctx.font = 'bold 10px sans-serif';
|
||||
ctx.fillText(status.text, textX + ctx.measureText(this.truncateText(ctx, item.title || '未命名', maxW - 60)).width + 8 + statusW / 2, y + 24);
|
||||
|
||||
// 数据统计
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.45)';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`▶ ${this.formatNumber(item.play_count || 0)}`, textX, y + 50);
|
||||
ctx.fillText(`♥ ${this.formatNumber(item.like_count || 0)}`, textX + 55, y + 50);
|
||||
ctx.fillText(`💰 ${(item.earnings || 0).toFixed(1)}`, textX + 105, y + 50);
|
||||
|
||||
// 操作按钮
|
||||
const btnY = y + 65;
|
||||
const btns = ['编辑', '数据'];
|
||||
btns.forEach((btn, i) => {
|
||||
const btnX = textX + i * 55;
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.1)';
|
||||
this.roundRect(ctx, btnX, btnY, 48, 24, 12);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.7)';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(btn, btnX + 24, btnY + 16);
|
||||
});
|
||||
}
|
||||
|
||||
renderDraftCard(ctx, item, x, y, w, h, index) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.05)';
|
||||
this.roundRect(ctx, x, y, w, h, 12);
|
||||
ctx.fill();
|
||||
|
||||
const coverW = 70, coverH = h - 16;
|
||||
const colors = this.getGradientColors(index);
|
||||
const coverGradient = ctx.createLinearGradient(x + 8, y + 8, x + 8 + coverW, y + 8 + coverH);
|
||||
coverGradient.addColorStop(0, colors[0]);
|
||||
coverGradient.addColorStop(1, colors[1]);
|
||||
ctx.fillStyle = coverGradient;
|
||||
this.roundRect(ctx, x + 8, y + 8, coverW, coverH, 10);
|
||||
ctx.fill();
|
||||
|
||||
// AI标签
|
||||
if (item.source === 'ai') {
|
||||
ctx.fillStyle = '#a855f7';
|
||||
this.roundRect(ctx, x + 8, y + 8, 28, 16, 8);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('AI', x + 22, y + 19);
|
||||
}
|
||||
|
||||
const textX = x + 88;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(this.truncateText(ctx, item.title || '未命名草稿', w - 180), textX, y + 25);
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.fillText(`创建于 ${item.created_at || '刚刚'}`, textX, y + 48);
|
||||
ctx.fillText(`${item.node_count || 0} 个节点`, textX + 100, y + 48);
|
||||
|
||||
// 按钮
|
||||
const btnY = y + 62;
|
||||
const btns = [{ text: '继续编辑', primary: true }, { text: '删除', primary: false }];
|
||||
let btnX = textX;
|
||||
btns.forEach((btn) => {
|
||||
const btnW = btn.primary ? 65 : 45;
|
||||
if (btn.primary) {
|
||||
const btnGradient = ctx.createLinearGradient(btnX, btnY, btnX + btnW, btnY);
|
||||
btnGradient.addColorStop(0, '#a855f7');
|
||||
btnGradient.addColorStop(1, '#ec4899');
|
||||
ctx.fillStyle = btnGradient;
|
||||
} else {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.1)';
|
||||
}
|
||||
this.roundRect(ctx, btnX, btnY, btnW, 26, 13);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = btn.primary ? 'bold 11px sans-serif' : '11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(btn.text, btnX + btnW / 2, btnY + 17);
|
||||
btnX += btnW + 8;
|
||||
});
|
||||
}
|
||||
|
||||
renderSimpleCard(ctx, item, x, y, w, h, index) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.05)';
|
||||
this.roundRect(ctx, x, y, w, h, 12);
|
||||
ctx.fill();
|
||||
|
||||
const coverW = 60, coverH = h - 16;
|
||||
const colors = this.getGradientColors(index);
|
||||
const coverGradient = ctx.createLinearGradient(x + 8, y + 8, x + 8 + coverW, y + 8 + coverH);
|
||||
coverGradient.addColorStop(0, colors[0]);
|
||||
coverGradient.addColorStop(1, colors[1]);
|
||||
ctx.fillStyle = coverGradient;
|
||||
this.roundRect(ctx, x + 8, y + 8, coverW, coverH, 8);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.85)';
|
||||
ctx.font = 'bold 9px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(item.category || '故事', x + 8 + coverW / 2, y + 8 + coverH / 2 + 3);
|
||||
|
||||
const textX = x + 78;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(this.truncateText(ctx, item.story_title || item.title || '未知', w - 150), textX, y + 28);
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.45)';
|
||||
ctx.font = '11px sans-serif';
|
||||
if (this.currentTab === 3 && item.is_completed) {
|
||||
ctx.fillStyle = '#4ade80';
|
||||
ctx.fillText('✓ 已完成', textX, y + 50);
|
||||
} else if (this.currentTab === 3) {
|
||||
ctx.fillText('进行中...', textX, y + 50);
|
||||
} else {
|
||||
ctx.fillText(item.category || '', textX, y + 50);
|
||||
}
|
||||
|
||||
// 继续按钮
|
||||
const btnGradient = ctx.createLinearGradient(x + w - 58, y + 28, x + w - 10, y + 28);
|
||||
btnGradient.addColorStop(0, '#ff6b6b');
|
||||
btnGradient.addColorStop(1, '#ffd700');
|
||||
ctx.fillStyle = btnGradient;
|
||||
this.roundRect(ctx, x + w - 58, y + 28, 48, 26, 13);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('继续', x + w - 34, y + 45);
|
||||
}
|
||||
|
||||
getGradientColors(index) {
|
||||
const colors = [
|
||||
['#ff758c', '#ff7eb3'],
|
||||
['#667eea', '#764ba2'],
|
||||
['#4facfe', '#00f2fe'],
|
||||
['#43e97b', '#38f9d7'],
|
||||
['#fa709a', '#fee140']
|
||||
['#fa709a', '#fee140'],
|
||||
['#a855f7', '#ec4899']
|
||||
];
|
||||
const colors = coverColors[index % coverColors.length];
|
||||
const coverGradient = ctx.createLinearGradient(x + 12, y + 12, x + 12 + coverSize, y + 12 + coverSize);
|
||||
coverGradient.addColorStop(0, colors[0]);
|
||||
coverGradient.addColorStop(1, colors[1]);
|
||||
ctx.fillStyle = coverGradient;
|
||||
this.roundRect(ctx, x + 12, y + 12, coverSize, coverSize, 10);
|
||||
ctx.fill();
|
||||
|
||||
// 封面文字
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.9)';
|
||||
ctx.font = 'bold 10px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(item.category || '故事', x + 12 + coverSize / 2, y + 12 + coverSize / 2 + 4);
|
||||
|
||||
// 标题
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 15px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
const title = item.story_title || item.title || '未知故事';
|
||||
ctx.fillText(title.length > 12 ? title.substring(0, 12) + '...' : title, x + 90, y + 35);
|
||||
|
||||
// 状态
|
||||
ctx.font = '12px sans-serif';
|
||||
if (this.currentTab === 1 && item.is_completed) {
|
||||
ctx.fillStyle = '#4ecca3';
|
||||
ctx.fillText('✓ 已完成', x + 90, y + 58);
|
||||
} else if (this.currentTab === 1) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
||||
ctx.fillText('进行中...', x + 90, y + 58);
|
||||
} else {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
||||
ctx.fillText(item.category || '', x + 90, y + 58);
|
||||
}
|
||||
|
||||
// 继续按钮
|
||||
const btnGradient = ctx.createLinearGradient(x + width - 65, y + 30, x + width - 10, y + 30);
|
||||
btnGradient.addColorStop(0, '#ff6b6b');
|
||||
btnGradient.addColorStop(1, '#ffd700');
|
||||
ctx.fillStyle = btnGradient;
|
||||
this.roundRect(ctx, x + width - 65, y + 30, 52, 28, 14);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 12px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('继续', x + width - 39, y + 49);
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
|
||||
// 圆角矩形
|
||||
roundRect(ctx, x, y, width, height, radius) {
|
||||
formatNumber(num) {
|
||||
if (!num) return '0';
|
||||
if (num >= 10000) return (num / 10000).toFixed(1) + 'w';
|
||||
if (num >= 1000) return (num / 1000).toFixed(1) + 'k';
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
truncateText(ctx, text, maxW) {
|
||||
if (!text) return '';
|
||||
if (ctx.measureText(text).width <= maxW) return text;
|
||||
let t = text;
|
||||
while (t.length > 0 && ctx.measureText(t + '...').width > maxW) t = t.slice(0, -1);
|
||||
return t + '...';
|
||||
}
|
||||
|
||||
roundRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.lineTo(x + width - radius, y);
|
||||
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
||||
ctx.lineTo(x + width, y + height - radius);
|
||||
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
||||
ctx.lineTo(x + radius, y + height);
|
||||
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
||||
ctx.lineTo(x, y + radius);
|
||||
ctx.quadraticCurveTo(x, y, x + radius, y);
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.lineTo(x + w - r, y);
|
||||
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
||||
ctx.lineTo(x + w, y + h - r);
|
||||
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
||||
ctx.lineTo(x + r, y + h);
|
||||
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
||||
ctx.lineTo(x, y + r);
|
||||
ctx.quadraticCurveTo(x, y, x + r, y);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
@@ -305,11 +516,7 @@ export default class ProfileScene extends BaseScene {
|
||||
if (!this.isDragging) return;
|
||||
const touch = e.touches[0];
|
||||
const deltaY = this.lastTouchY - touch.clientY;
|
||||
|
||||
if (Math.abs(touch.clientY - this.touchStartY) > 5) {
|
||||
this.hasMoved = true;
|
||||
}
|
||||
|
||||
if (Math.abs(touch.clientY - this.touchStartY) > 5) this.hasMoved = true;
|
||||
this.scrollVelocity = deltaY;
|
||||
this.scrollY += deltaY;
|
||||
this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScrollY));
|
||||
@@ -318,29 +525,39 @@ export default class ProfileScene extends BaseScene {
|
||||
|
||||
onTouchEnd(e) {
|
||||
this.isDragging = false;
|
||||
|
||||
if (this.hasMoved) return;
|
||||
|
||||
const touch = e.changedTouches[0];
|
||||
const x = touch.clientX;
|
||||
const y = touch.clientY;
|
||||
|
||||
// 返回按钮
|
||||
// 返回
|
||||
if (y < 50 && x < 80) {
|
||||
this.main.sceneManager.switchScene('home');
|
||||
return;
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
if (y >= 240 && y <= 285) {
|
||||
const tabWidth = (this.screenWidth - 30) / 2;
|
||||
const newTab = x < 15 + tabWidth ? 0 : 1;
|
||||
if (newTab !== this.currentTab) {
|
||||
this.currentTab = newTab;
|
||||
this.scrollY = 0;
|
||||
this.calculateMaxScroll();
|
||||
if (this.tabRects) {
|
||||
for (const rect of this.tabRects) {
|
||||
if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) {
|
||||
if (this.currentTab !== rect.index) {
|
||||
this.currentTab = rect.index;
|
||||
this.scrollY = 0;
|
||||
this.calculateMaxScroll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创作按钮
|
||||
if (this.createBtnRect && this.currentTab === 0) {
|
||||
const btn = this.createBtnRect;
|
||||
if (x >= btn.x && x <= btn.x + btn.width && y >= btn.y && y <= btn.y + btn.height) {
|
||||
this.main.sceneManager.switchScene('aiCreate');
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 卡片点击
|
||||
@@ -348,24 +565,26 @@ export default class ProfileScene extends BaseScene {
|
||||
}
|
||||
|
||||
handleCardClick(x, y) {
|
||||
const list = this.currentTab === 0 ? this.collections : this.progress;
|
||||
const startY = 295;
|
||||
const cardHeight = 90;
|
||||
const cardMargin = 12;
|
||||
const padding = 15;
|
||||
const list = this.getCurrentList();
|
||||
const startY = 250;
|
||||
const cardH = this.currentTab <= 1 ? 100 : 85;
|
||||
const gap = 10;
|
||||
|
||||
const adjustedY = y + this.scrollY;
|
||||
const index = Math.floor((adjustedY - startY) / (cardHeight + cardMargin));
|
||||
const index = Math.floor((adjustedY - startY) / (cardH + gap));
|
||||
|
||||
if (index >= 0 && index < list.length) {
|
||||
const item = list[index];
|
||||
const storyId = item.story_id || item.id;
|
||||
const cardY = startY + index * (cardHeight + cardMargin) - this.scrollY;
|
||||
const buttonX = padding + (this.screenWidth - padding * 2) - 65;
|
||||
|
||||
if (x >= buttonX && x <= buttonX + 52 && y >= cardY + 30 && y <= cardY + 58) {
|
||||
if (this.currentTab >= 2) {
|
||||
// 收藏/记录 - 跳转播放
|
||||
this.main.sceneManager.switchScene('story', { storyId });
|
||||
} else if (this.currentTab === 1) {
|
||||
// 草稿 - 跳转编辑(暂用AI创作)
|
||||
this.main.sceneManager.switchScene('aiCreate', { draftId: item.id });
|
||||
}
|
||||
// 作品Tab的按钮操作需要更精确判断,暂略
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user