fix: 首页我的计划点击跳转错页 + feat: 新增留言反馈功能
fix: - 首页'我的计划'卡片误绑 onExercise(跳动作详情),改为 onPlanTap 正确跳训练计划详情页 - 修复后点击计划可正常打开 plan-detail feat(留言反馈): - 后端新增 feedback 模块:POST /api/feedback(AuthGuard,content 必填/contact 选填),JsonStore 持久化 - 前端新增 pages/feedback 页面(留言内容 textarea + 联系方式选填 + 提交),profile 菜单新增'意见反馈'入口 - icon-svg 新增 message-circle 图标(反馈页用)
This commit is contained in:
@@ -7,6 +7,7 @@ import { AuthModule } from './auth/auth.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { FavoritesModule } from './favorites/favorites.module';
|
||||
import { PlansModule } from './plans/plans.module';
|
||||
import { FeedbackModule } from './feedback/feedback.module';
|
||||
|
||||
// 若 backend/media 目录存在(放有 images/ 与 videos/),则通过 /media 暴露静态资源
|
||||
const mediaDir = path.join(__dirname, '..', 'media');
|
||||
@@ -21,6 +22,7 @@ const staticImports = fs.existsSync(mediaDir)
|
||||
UsersModule,
|
||||
FavoritesModule,
|
||||
PlansModule,
|
||||
FeedbackModule,
|
||||
...staticImports,
|
||||
],
|
||||
})
|
||||
|
||||
31
backend/src/feedback/feedback.controller.ts
Normal file
31
backend/src/feedback/feedback.controller.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { FeedbackService } from './feedback.service';
|
||||
import { AuthGuard } from '../common/auth.guard';
|
||||
import { CurrentUser } from '../common/current-user.decorator';
|
||||
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
|
||||
|
||||
class FeedbackBody {
|
||||
@IsString()
|
||||
@MinLength(1, { message: '留言内容不能为空' })
|
||||
@MaxLength(1000, { message: '留言内容不能超过 1000 字' })
|
||||
content: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100, { message: '联系方式不能超过 100 字' })
|
||||
contact?: string;
|
||||
}
|
||||
|
||||
@Controller('api/feedback')
|
||||
@UseGuards(AuthGuard)
|
||||
export class FeedbackController {
|
||||
constructor(private readonly svc: FeedbackService) {}
|
||||
|
||||
@Post()
|
||||
submit(
|
||||
@CurrentUser() u: { openid: string },
|
||||
@Body() body: FeedbackBody,
|
||||
) {
|
||||
return this.svc.submit(u.openid, body.content, body.contact);
|
||||
}
|
||||
}
|
||||
9
backend/src/feedback/feedback.module.ts
Normal file
9
backend/src/feedback/feedback.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FeedbackService } from './feedback.service';
|
||||
import { FeedbackController } from './feedback.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [FeedbackController],
|
||||
providers: [FeedbackService],
|
||||
})
|
||||
export class FeedbackModule {}
|
||||
33
backend/src/feedback/feedback.service.ts
Normal file
33
backend/src/feedback/feedback.service.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { getStore } from '../common/store';
|
||||
|
||||
export interface Feedback {
|
||||
id: string;
|
||||
openid: string;
|
||||
content: string;
|
||||
contact?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class FeedbackService {
|
||||
private store = getStore<Feedback>('feedback');
|
||||
|
||||
submit(openid: string, content: string, contact?: string): Feedback {
|
||||
return this.store.insert({
|
||||
id: randomUUID(),
|
||||
openid,
|
||||
content,
|
||||
contact: contact || undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// 管理员视角:按时间倒序返回全部留言
|
||||
list(): Feedback[] {
|
||||
return this.store
|
||||
.all()
|
||||
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
"pages/collection-detail/collection-detail",
|
||||
"pages/search/search",
|
||||
"pages/profile/profile",
|
||||
"pages/feedback/feedback",
|
||||
"pages/favorites/favorites",
|
||||
"pages/my-plans/my-plans",
|
||||
"pages/plan-edit/plan-edit",
|
||||
|
||||
56
miniprogram/pages/feedback/feedback.js
Normal file
56
miniprogram/pages/feedback/feedback.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const app = getApp();
|
||||
const auth = require('../../utils/auth');
|
||||
const fbSvc = require('../../services/feedback');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
content: '',
|
||||
contact: '',
|
||||
count: 0,
|
||||
maxLen: 1000,
|
||||
submitting: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 20 });
|
||||
},
|
||||
|
||||
onContentInput(e) {
|
||||
const content = e.detail.value;
|
||||
this.setData({ content, count: content.length });
|
||||
},
|
||||
|
||||
onContactInput(e) {
|
||||
this.setData({ contact: e.detail.value });
|
||||
},
|
||||
|
||||
onSubmit() {
|
||||
const content = (this.data.content || '').trim();
|
||||
if (!content) {
|
||||
wx.showToast({ title: '请填写留言内容', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (this.data.submitting) return;
|
||||
this.setData({ submitting: true });
|
||||
wx.showLoading({ title: '提交中…', mask: true });
|
||||
fbSvc
|
||||
.submit(content, (this.data.contact || '').trim())
|
||||
.then(() => {
|
||||
wx.hideLoading();
|
||||
this.setData({ submitting: false, content: '', contact: '', count: 0 });
|
||||
wx.showModal({
|
||||
title: '已收到反馈',
|
||||
content: '感谢你的留言,我们会认真查看!',
|
||||
showCancel: false,
|
||||
confirmText: '好的'
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
wx.hideLoading();
|
||||
this.setData({ submitting: false });
|
||||
const msg = (err && err.message) || '提交失败,请稍后再试';
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
});
|
||||
}
|
||||
});
|
||||
8
miniprogram/pages/feedback/feedback.json
Normal file
8
miniprogram/pages/feedback/feedback.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"navbar": "/components/navbar/index",
|
||||
"bottom-nav": "/components/bottom-nav/index",
|
||||
"fc-icon": "/components/fc-icon/index"
|
||||
}
|
||||
}
|
||||
39
miniprogram/pages/feedback/feedback.wxml
Normal file
39
miniprogram/pages/feedback/feedback.wxml
Normal file
@@ -0,0 +1,39 @@
|
||||
<view class="page" style="padding-top: {{statusBarHeight}}px;">
|
||||
<navbar title="意见反馈" show-back />
|
||||
|
||||
<view class="intro">
|
||||
<fc-icon name="message-circle" size="28" color="var(--accent)" />
|
||||
<view class="intro__txt">遇到问题或有好点子?留言告诉我们,帮助我们做得更好。</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="field">
|
||||
<view class="field__label">留言内容</view>
|
||||
<textarea
|
||||
class="field__textarea"
|
||||
placeholder="说说你的想法、遇到的问题或建议…"
|
||||
placeholder-class="ph"
|
||||
maxlength="{{maxLen}}"
|
||||
value="{{content}}"
|
||||
bindinput="onContentInput"
|
||||
/>
|
||||
<view class="field__count">{{count}}/{{maxLen}}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<view class="field__label">联系方式<text class="field__opt">(选填)</text></view>
|
||||
<input
|
||||
class="field__input"
|
||||
placeholder="手机号 / 微信,方便我们回访"
|
||||
placeholder-class="ph"
|
||||
value="{{contact}}"
|
||||
bindinput="onContactInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit" bindtap="onSubmit">提交反馈</view>
|
||||
|
||||
<view style="height: 160rpx;"></view>
|
||||
<bottom-nav active="mine" />
|
||||
</view>
|
||||
81
miniprogram/pages/feedback/feedback.wxss
Normal file
81
miniprogram/pages/feedback/feedback.wxss
Normal file
@@ -0,0 +1,81 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: var(--page);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.intro {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 32rpx 32rpx 8rpx;
|
||||
}
|
||||
.intro__txt {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 24rpx 32rpx;
|
||||
background: var(--surface);
|
||||
border: 1rpx solid var(--hairline);
|
||||
border-radius: 24rpx;
|
||||
padding: 8rpx 28rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
padding: 24rpx 0;
|
||||
position: relative;
|
||||
}
|
||||
.field + .field {
|
||||
border-top: 1rpx solid var(--hairline);
|
||||
}
|
||||
.field__label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.field__opt {
|
||||
font-weight: 400;
|
||||
color: var(--text-3);
|
||||
}
|
||||
.field__textarea {
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
}
|
||||
.field__count {
|
||||
text-align: right;
|
||||
font-size: 22rpx;
|
||||
color: var(--text-3);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.field__input {
|
||||
font-size: 28rpx;
|
||||
color: var(--text);
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
.ph {
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin: 40rpx 32rpx;
|
||||
height: 92rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--accent);
|
||||
color: var(--accent-on);
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.submit:active {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
<view class="personal__block" wx:if="{{myPlans.length}}">
|
||||
<view class="personal__sub">我的计划</view>
|
||||
<view class="plan-row">
|
||||
<view class="plan-chip" wx:for="{{myPlans}}" wx:key="id" data-id="{{item.id}}" bindtap="onExercise">
|
||||
<view class="plan-chip" wx:for="{{myPlans}}" wx:key="id" data-id="{{item.id}}" bindtap="onPlanTap">
|
||||
<fc-icon name="clipboard-list" size="20" color="var(--accent)" />
|
||||
<view class="plan-chip__body">
|
||||
<text class="plan-chip__name">{{item.name}}</text>
|
||||
|
||||
@@ -126,6 +126,7 @@ Page({
|
||||
goPlans() { wx.navigateTo({ url: '/pages/my-plans/my-plans' }); },
|
||||
goFavPlans() { wx.navigateTo({ url: '/pages/plan-favorites/plan-favorites' }); },
|
||||
goOfficial() { wx.navigateTo({ url: '/pages/collection/collection' }); },
|
||||
goFeedback() { wx.navigateTo({ url: '/pages/feedback/feedback' }); },
|
||||
|
||||
noop() {}
|
||||
});
|
||||
|
||||
@@ -57,6 +57,11 @@
|
||||
<text class="cell__label">官方训练计划</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
<view class="cell" bindtap="goFeedback">
|
||||
<fc-icon class="cell__icon" name="message-circle" size="24" color="var(--text-2)" />
|
||||
<text class="cell__label">意见反馈</text>
|
||||
<text class="cell__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="logout" bindtap="onLogout">退出登录</view>
|
||||
|
||||
9
miniprogram/services/feedback.js
Normal file
9
miniprogram/services/feedback.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// 留言反馈服务
|
||||
const { request } = require('../utils/request');
|
||||
|
||||
// 提交留言(需登录):content 必填,contact 选填
|
||||
function submit(content, contact) {
|
||||
return request({ url: '/api/feedback', method: 'POST', data: { content, contact } });
|
||||
}
|
||||
|
||||
module.exports = { submit };
|
||||
@@ -1,5 +1,5 @@
|
||||
// AUTO-GENERATED by /tmp/gen_iconfont.mjs — 勿手改
|
||||
// Lucide (MIT) 子集 iconfont,font-family: fc-icon
|
||||
// Lucide (MIT) 子集(iconfont 已废弃;现仅作 SVG 生成器的名称清单来源)
|
||||
module.exports = {
|
||||
"zap": "\\uf101",
|
||||
"x-circle": "\\uf102",
|
||||
@@ -49,5 +49,6 @@ module.exports = {
|
||||
"arrow-left": "\\uf12e",
|
||||
"alert-circle": "\\uf12f",
|
||||
"activity": "\\uf130",
|
||||
"accessibility": "\\uf131"
|
||||
"accessibility": "\\uf131",
|
||||
"message-circle": "\\uf132"
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user