diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index a8f5dfa..e29cab4 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -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, ], }) diff --git a/backend/src/feedback/feedback.controller.ts b/backend/src/feedback/feedback.controller.ts new file mode 100644 index 0000000..da27549 --- /dev/null +++ b/backend/src/feedback/feedback.controller.ts @@ -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); + } +} diff --git a/backend/src/feedback/feedback.module.ts b/backend/src/feedback/feedback.module.ts new file mode 100644 index 0000000..72e6d60 --- /dev/null +++ b/backend/src/feedback/feedback.module.ts @@ -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 {} diff --git a/backend/src/feedback/feedback.service.ts b/backend/src/feedback/feedback.service.ts new file mode 100644 index 0000000..e398a98 --- /dev/null +++ b/backend/src/feedback/feedback.service.ts @@ -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'); + + 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)); + } +} diff --git a/miniprogram/app.json b/miniprogram/app.json index 9e421fb..e5a9976 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -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", diff --git a/miniprogram/pages/feedback/feedback.js b/miniprogram/pages/feedback/feedback.js new file mode 100644 index 0000000..6529bee --- /dev/null +++ b/miniprogram/pages/feedback/feedback.js @@ -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' }); + }); + } +}); diff --git a/miniprogram/pages/feedback/feedback.json b/miniprogram/pages/feedback/feedback.json new file mode 100644 index 0000000..cef796e --- /dev/null +++ b/miniprogram/pages/feedback/feedback.json @@ -0,0 +1,8 @@ +{ + "navigationStyle": "custom", + "usingComponents": { + "navbar": "/components/navbar/index", + "bottom-nav": "/components/bottom-nav/index", + "fc-icon": "/components/fc-icon/index" + } +} diff --git a/miniprogram/pages/feedback/feedback.wxml b/miniprogram/pages/feedback/feedback.wxml new file mode 100644 index 0000000..b2e97cb --- /dev/null +++ b/miniprogram/pages/feedback/feedback.wxml @@ -0,0 +1,39 @@ + + + + + + 遇到问题或有好点子?留言告诉我们,帮助我们做得更好。 + + + + + 留言内容 +