blob: 7a1b63112315f30b19abde544e8a7a1635bafeac [file] [log] [blame]
'use client';
import React, { useEffect, useState, useRef } from 'react';
import { TabView, TabPanel } from 'primereact/tabview';
import { Avatar } from 'primereact/avatar';
import { Button } from 'primereact/button';
import { Card } from 'primereact/card';
import {Image} from "primereact/image";
// 发布资源
import { Dialog } from 'primereact/dialog';
import {InputText} from "primereact/inputtext";
import {InputTextarea} from "primereact/inputtextarea";
import {FileUpload} from "primereact/fileupload";
// 资源分类
import { RadioButton, RadioButtonChangeEvent } from "primereact/radiobutton";
// 资源标签
import { MultiSelect, MultiSelectChangeEvent } from 'primereact/multiselect';
// 浮动按钮
import { SpeedDial } from 'primereact/speeddial';
// 评分图标
import { Fire } from '@icon-park/react';
// 消息提醒
import { Toast } from 'primereact/toast';
// 页面跳转
import { useRouter } from "next/navigation";
// 接口传输
import axios from "axios";
// 样式
import './user.scss';
import {toNumber} from "lodash";
// 用户信息
interface UserInfo {
userId: number;
username: string;
password: string;
avatar: string;
followerCount: number;// 粉丝数
subscriberCount: number;// 关注数
signature: string;// 个性签名
uploadAmount: number;
purchaseAmount: number;
credits: number;
}
// 用户数据
interface UserData {
subscriberCount: number; // 关注数
uploadAmount: number; // 上传量(资源个数)
beDownloadedAmount: number; // 上传资源被下载量
seedPercentageList: number[]; // 上传资源类型百分比列表,按材质包、模组、整合包、地图的顺序返回
}
// 用户发布过的资源
interface Resource {
resourceId: number;
resourceName: string;
resourcePicture: string;
resourceSummary: string; // 资源简介(一句话)
resourceDetail: string; // 资源介绍
uploadTime: string; // 上传时间
lastUpdateTime: string; // 最近更新时间
price: number;
downloads: number;
likes: number;
collections: number;
comments: number;
seeds: number; // 种子数
classify: string; // 资源分类(材质包:resourcePack,模组:mod,整合包:modPack ,地图:map
}
// 用户发布过的资源列表
interface ResourceList {
records: Resource[];
}
// 资源标签
interface GameplayOption {
name: string;
code: number;
}
// 资源标签选项
const gameplayOptions: GameplayOption[] = [
{ name: '科技', code: 1 },
{ name: '魔法', code: 2 },
{ name: '建筑', code: 3 },
{ name: '风景', code: 4 },
{ name: '竞技', code: 5 },
{ name: '生存', code: 6 },
{ name: '冒险', code: 7 },
{ name: '跑酷', code: 8 },
{ name: '艺术', code: 9 },
{ name: '剧情', code: 10 },
{ name: '社交', code: 11 },
{ name: '策略', code: 12 },
{ name: '极限', code: 13 }
];
export default function UserPage() {
// 路由
const router = useRouter();
// 发布资源列表
const [resourceList, setResourceList] = useState<Resource[]>([]);
// 用户信息
const [userInfo, setUserInfo] = useState<UserInfo>();
// 用户数据
const [userData, setUserData] = useState<UserData>();
// 消息提醒
const toast = useRef<Toast>(null);
// 资源标签
const [selectedGameplay, setSelectedGameplay] = useState<GameplayOption[]>([]);
// 判断用户是否上传资源封面
const [isUploadPicture, setIsUploadPicture] = useState<boolean>(false);
useEffect(() => {
fetchUserInfo();
fetchUserData();
fetchResourceList();
}, []);
// 获取用户信息
const fetchUserInfo = async () => {
try {
const response = await axios.get<UserInfo>(process.env.PUBLIC_URL + `/user/info`, {
params: { userId: 22301010 }
});
console.log('获取用户信息:', response.data);
setUserInfo(response.data);
} catch (err) {
console.error('获取用户信息失败', err);
toast.current?.show({ severity: 'error', summary: 'error', detail: '获取用户信息失败' });
}
};
// 获取用户数据
const fetchUserData = async () => {
try {
const response = await axios.get<UserData>(process.env.PUBLIC_URL + `/user/data`, {
params: { userId: 22301010 }
});
console.log('获取用户数据:', response.data);
setUserData(response.data);
} catch (err) {
console.error('获取用户数据失败', err);
toast.current?.show({ severity: 'error', summary: 'error', detail: '获取用户数据失败' });
}
};
// 格式化数字显示 (3000 -> 3k)
const formatCount = (count?: number): string => {
if (count == null) return "0"; // 同时处理 undefined/null
const absCount = Math.abs(count); // 处理负数
const format = (num: number, suffix: string) => {
const fixed = num.toFixed(1);
return fixed.endsWith('.0')
? `${Math.floor(num)}${suffix}`
: `${fixed}${suffix}`;
};
if (absCount >= 1e6) return format(count / 1e6, "m");
if (absCount >= 1e3) return format(count / 1e3, "k");
return count.toString();
};
// 获取发布资源
const fetchResourceList = async () => {
try {
const response = await axios.get<ResourceList>(process.env.PUBLIC_URL +`/user/upload`, {
params: { userId: 22301010, pageNumber: 1, rows: 3 }
});
console.log('获取发布资源列表:', response.data.records);
setResourceList(response.data.records);
// const imgUrl = `${process.env.NEXT_PUBLIC_NGINX_URL}/${resourceList[0].resourcePicture}`;
// console.log("Image URL:", imgUrl);
} catch (err) {
console.error('获取发布资源失败', err);
toast.current?.show({ severity: 'error', summary: 'error', detail: '获取发布资源失败' });
}
};
// 浮动按钮的子模块
const actions = [
{
template: () => (
<Button label="管理资源" onClick={() => router.push(`/user/manage/resources/`)}/>
)
},
{
template: () => (
<Button label="已购资源" onClick={() => router.push(`/user/purchased-resources/`)}/>
)
},
{
template: () => (
<Button label="发布资源" onClick={() => setVisible(true)}/>
)
},
{
template: () => (
<Button label="编辑悬赏" onClick={() => router.push(`/user/manage/resources/`)}/>
)
}
];
// 发布资源弹窗
const [visible, setVisible] = useState(false);
const [resourceFormData, setResourceFormData] = useState({
resource: {
resourceName: '',
resourcePicture: '',
resourceSummary: '',
resourceDetail: '',
uploadTime: '',
lastUpdateTime: '',
price: '',
classify: '',
},
gameplayList: [''],
completeRewardId: null,
userId: 0,
});
const [ingredient, setIngredient] = useState<string>('');
// 图片上传消息通知
const onUpload = () => {
setIsUploadPicture(true);
toast.current?.show({ severity: 'info', summary: 'Success', detail: 'File Uploaded' });
};
// 上传资源接口
const handleSubmit = async () => {
try {
// 规定用户必须输入的内容
if (resourceFormData.resource.resourceName == '') {
toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源名称' });
return;
}
if (resourceFormData.resource.resourceSummary == '') {
toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源简介' });
return;
}
if (resourceFormData.resource.price == '') {
toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源价格' });
return;
}
if (resourceFormData.resource.classify == '') {
toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源分类' });
return;
}
if (
resourceFormData.gameplayList.length === 1 &&
resourceFormData.gameplayList[0] === ''
) {
toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源标签' });
return;
}
if (!isUploadPicture) {
toast.current?.show({ severity: 'info', summary: 'error', detail: '缺少资源封面' });
return;
}
const currentDate = new Date().toISOString().split('T')[0];
const postData = {
resource: {
resourceName: resourceFormData.resource.resourceName,
resourcePicture: resourceFormData.resource.resourcePicture,
resourceSummary: resourceFormData.resource.resourceSummary,
resourceDetail: resourceFormData.resource.resourceDetail,
uploadTime: currentDate,
lastUpdateTime: currentDate,
price: toNumber(resourceFormData.resource.price),
classify: resourceFormData.resource.classify,
},
gameplayList: resourceFormData.gameplayList,
completeRewardId: null,
userId: 22301010, // 记得用户登录状态获取
};
// 发送POST请求
const response = await axios.post(process.env.PUBLIC_URL + '/resource', postData);
console.log("上传资源的信息:", postData);
if (response.status === 200) {
toast.current?.show({ severity: 'success', summary: 'Success', detail: '资源上传成功' });
// 上传成功
setVisible(false);
// 重置表单
setResourceFormData({
resource: {
resourceName: '',
resourcePicture: '',
resourceSummary: '',
resourceDetail: '',
uploadTime: '',
lastUpdateTime: '',
price: '',
classify: '',
},
gameplayList: [],
completeRewardId: null,
userId: 0,
});
// 重置资源分类
setIngredient("");
// 重置资源标签
setSelectedGameplay([]);
// 重置上传封面状态
setIsUploadPicture(false);
// 可以刷新资源列表
// fetchResourceList();
}
} catch (error) {
console.error('资源上传失败:', error);
toast.current?.show({ severity: 'error', summary: 'error', detail: '资源上传失败' });
}
};
return (
<div className="user-container">
<Toast ref={toast}></Toast>
{/*个人信息*/}
<div className="user-profile-card">
<Avatar
image={`${process.env.NEXT_PUBLIC_NGINX_URL}/users/${userInfo?.avatar}`}
className="user-avatar"
shape="circle"
/>
<div className="user-info">
<div className="user-detail-info">
<div className="name-container">
<h2 className="name">{userInfo?.username}</h2>
<span className="signature">{userInfo?.signature}</span>
</div>
<div className="stats-container">
<div className="stats">
<span className="stats-label">粉丝:</span>
<span className="stats-value">{userInfo?.followerCount}</span>
</div>
<div className="stats">
<span className="stats-label">累计上传量:</span>
<span className="stats-value">{formatCount(userData?.uploadAmount)}</span>
</div>
<div className="stats">
<span className="stats-label">关注:</span>
<span className="stats-value">{userInfo?.subscriberCount}</span>
</div>
<div className="stats">
<span className="stats-label">累计被下载量:</span>
<span className="stats-value">{formatCount(userData?.beDownloadedAmount)}</span>
</div>
</div>
</div>
<Button label="关注" className="action-button"/>
</div>
</div>
{/*个人内容*/}
<TabView>
<TabPanel header="主页">
{/*推荐资源*/}
<div className="homepage-item">
<div className="section-header">
<h1>推荐资源</h1>
<Button
label="显示更多"
link
onClick={() => router.push('/resource/recommend/模组')}
/>
</div>
<div className="resource-grid">
{/*{mods.map((mod) => (*/}
{/* <Card key={mod.resourceId} className="resource-card" onClick={() => router.push(`/resource/resource-detail/${mod.resourceId}`)}>*/}
{/* <Image*/}
{/* src={process.env.NEXT_PUBLIC_NGINX_URL + mod.resourcePicture}*/}
{/* alt={mod.resourceName}*/}
{/* width="368"*/}
{/* height="200"*/}
{/* />*/}
{/* <div className="card-content">*/}
{/* <h3>{mod.resourceName}</h3>*/}
{/* <div className="view-count">*/}
{/* <Fire theme="outline" size="16" fill="#FF8D1A" />*/}
{/* <span>{mod.likes}</span>*/}
{/* </div>*/}
{/* </div>*/}
{/* </Card>*/}
{/*))}*/}
</div>
</div>
{/*发布资源*/}
<div className="homepage-item">
<div className="section-header">
<h1>发布资源</h1>
<Button
label="显示更多"
link
onClick={() => router.push('/user/manage/resources/')}
/>
</div>
<div className="resource-grid">
{resourceList.map((resourceList) => (
<Card key={resourceList.resourceId} className="resource-card" onClick={() => router.push(`/resource/resource-detail/${resourceList.resourceId}`)}>
<Image
src={process.env.NEXT_PUBLIC_NGINX_URL + resourceList.resourcePicture}
alt={resourceList.resourceName}
width="368"
height="200"
/>
<div className="card-content">
<h3>{resourceList.resourceName}</h3>
<div className="view-count">
<Fire theme="outline" size="16" fill="#FF8D1A" />
<span>{resourceList.likes}</span>
</div>
</div>
</Card>
))}
</div>
</div>
{/*发布帖子*/}
<div className="homepage-item">
<div className="section-header">
<h1>发布帖子</h1>
<Button
label="显示更多"
link
onClick={() => router.push('/resource/recommend/模组')}
/>
</div>
<div className="resource-grid">
{/*{mods.map((mod) => (*/}
{/* <Card key={mod.resourceId} className="resource-card" onClick={() => router.push(`/resource/resource-detail/${mod.resourceId}`)}>*/}
{/* <Image*/}
{/* src={process.env.NEXT_PUBLIC_NGINX_URL + mod.resourcePicture}*/}
{/* alt={mod.resourceName}*/}
{/* width="368"*/}
{/* height="200"*/}
{/* />*/}
{/* <div className="card-content">*/}
{/* <h3>{mod.resourceName}</h3>*/}
{/* <div className="view-count">*/}
{/* <Fire theme="outline" size="16" fill="#FF8D1A" />*/}
{/* <span>{mod.likes}</span>*/}
{/* </div>*/}
{/* </div>*/}
{/* </Card>*/}
{/*))}*/}
</div>
</div>
</TabPanel>
<TabPanel header="发布">
</TabPanel>
<TabPanel header="帖子">
</TabPanel>
<TabPanel header="收藏">
</TabPanel>
<TabPanel header="数据">
</TabPanel>
<TabPanel header="悬赏">
</TabPanel>
</TabView>
{/*浮动按钮*/}
<div className="card">
<SpeedDial
model={actions}
direction="up"
style={{ position: 'fixed', bottom: '2rem', right: '2rem' }}
showIcon="pi pi-plus"
hideIcon="pi pi-times"
buttonClassName="custom-speeddial-button"
/>
</div>
{/*发布资源弹窗*/}
<Dialog
header="发布资源"
visible={visible}
onHide={() => setVisible(false)}
className="publish-dialog"
modal
footer={
<div className="dialog-footer">
<Button label="发布" icon="pi pi-check" onClick={handleSubmit} autoFocus />
<Button label="取消" icon="pi pi-times" onClick={() => setVisible(false)} className="p-button-text" />
</div>
}
>
<div className="publish-form">
<div className="form-field">
<div className="form-field-header">
<span className="form-field-sign">*</span>
<label htmlFor="name">资源名称</label>
</div>
<InputText
id="name"
value={resourceFormData.resource.resourceName}
onChange={(e) => setResourceFormData(prev => ({
...prev, // 复制顶层所有属性
resource: {
...prev.resource, // 复制resource对象的所有属性
resourceName: e.target.value // 只更新resourceName
}
}))}
placeholder="请输入资源名称"
className="w-full"
required
/>
</div>
<div className="form-field">
<div className="form-field-header">
<span className="form-field-sign">*</span>
<label htmlFor="summary">资源简介</label>
</div>
<InputText
id="summary"
value={resourceFormData.resource.resourceSummary}
onChange={(e) => setResourceFormData(prev => ({
...prev, // 复制顶层所有属性
resource: {
...prev.resource, // 复制resource对象的所有属性
resourceSummary: e.target.value
}
}))}
placeholder="请输入资源简介(一句话)"
className="w-full"
required
/>
</div>
<div className="form-field">
<div className="form-field-header">
<label htmlFor="detail">资源介绍</label>
</div>
<InputTextarea
id="detail"
value={resourceFormData.resource.resourceDetail}
onChange={(e) => setResourceFormData(prev => ({
...prev, // 复制顶层所有属性
resource: {
...prev.resource, // 复制resource对象的所有属性
resourceDetail: e.target.value
}
}))}
rows={5}
placeholder="请输入资源介绍"
className="w-full"
required
/>
</div>
<div className="form-field">
<div className="form-field-header">
<span className="form-field-sign">*</span>
<label htmlFor="price">价格</label>
</div>
<InputText
id="price"
value={resourceFormData.resource.price}
onChange={(e) => setResourceFormData(prev => ({
...prev, // 复制顶层所有属性
resource: {
...prev.resource, // 复制resource对象的所有属性
price: e.target.value
}
}))}
placeholder="请输入资源价格"
className="w-full"
/>
</div>
<div className="form-field">
<div className="form-field-header">
<span className="form-field-sign">*</span>
<label htmlFor="classify">资源分类(请选择一项)</label>
</div>
<div className="form-field-classify">
<div className="flex align-items-center">
<RadioButton
inputId="ingredient1"
name="pizza"
value="resourcePack"
onChange={(e: RadioButtonChangeEvent) => {
setResourceFormData(prev => ({
...prev, // 复制顶层所有属性
resource: {
...prev.resource, // 复制resource对象的所有属性
classify: e.target.value
}
}));
setIngredient(e.value);
console.log(ingredient);
// console.log(resourceFormData.resource.classify);
}}
checked={ingredient === 'resourcePack'}
/>
<label htmlFor="ingredient1" className="ml-2">材质包</label>
</div>
<div className="flex align-items-center">
<RadioButton
inputId="ingredient2"
name="pizza"
value="modPack"
onChange={(e: RadioButtonChangeEvent) => {
setResourceFormData(prev => ({
...prev, // 复制顶层所有属性
resource: {
...prev.resource, // 复制resource对象的所有属性
classify: e.target.value
}
}));
setIngredient(e.value);
}}
// onChange={(e: RadioButtonChangeEvent) => setIngredient(e.value)}
checked={ingredient === 'modPack'}
/>
<label htmlFor="ingredient2" className="ml-2">整合包</label>
</div>
<div className="flex align-items-center">
<RadioButton
inputId="ingredient3"
name="pizza"
value="mod"
onChange={(e: RadioButtonChangeEvent) => {
setResourceFormData(prev => ({
...prev, // 复制顶层所有属性
resource: {
...prev.resource, // 复制resource对象的所有属性
classify: e.target.value
}
}));
setIngredient(e.value);
}}
// onChange={(e: RadioButtonChangeEvent) => setIngredient(e.value)}
checked={ingredient === 'mod'}
/>
<label htmlFor="ingredient3" className="ml-2">模组</label>
</div>
<div className="flex align-items-center">
<RadioButton
inputId="ingredient4"
name="pizza"
value="map"
onChange={(e: RadioButtonChangeEvent) => {
setResourceFormData(prev => ({
...prev, // 复制顶层所有属性
resource: {
...prev.resource, // 复制resource对象的所有属性
classify: e.target.value
}
}));
setIngredient(e.value);
}}
// onChange={(e: RadioButtonChangeEvent) => setIngredient(e.value)}
checked={ingredient === 'map'}
/>
<label htmlFor="ingredient4" className="ml-2">地图</label>
</div>
</div>
</div>
<div className="form-field">
<div className="form-field-header">
<span className="form-field-sign">*</span>
<label htmlFor="gameplayList">资源标签</label>
</div>
<MultiSelect
value={selectedGameplay}
onChange={(e: MultiSelectChangeEvent) => {
const selectedOptions = e.value as GameplayOption[];
// 提取选中项的 name 属性组成字符串数组
const selectedNames = selectedOptions.map(item => item.name);
setResourceFormData(prev => ({
...prev,
gameplayList: selectedNames
}));
setSelectedGameplay(selectedOptions);
}}
options={gameplayOptions}
display="chip"
optionLabel="name"
placeholder="请选择资源标签"
// maxSelectedLabels={3}
className="w-full md:w-20rem"
/>
</div>
<div className="form-field">
<div className="form-field-header">
<span className="form-field-sign">*</span>
<label>封面图片</label>
</div>
<FileUpload
mode="basic"
name="resource-image"
url={process.env.PUBLIC_URL +"/file"} // 与后端交互的URL
accept="image/*"
maxFileSize={10000000000}
chooseLabel="选择资源封面"
className="w-full"
onUpload={onUpload}
/>
</div>
</div>
</Dialog>
</div>
);
};