增加用户中心、用户注册登录
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// src/pages/list/basic-list/components/AddTimeLineItemModal.tsx
|
||||
import chinaRegion, { code2Location } from '@/commonConstant/chinaRegion';
|
||||
import { addStoryItem } from '@/pages/story/service';
|
||||
import { addStoryItem, updateStoryItem } from '@/pages/story/service';
|
||||
import { getImagesList } from '@/services/file/api'; // 引入获取图库图片的API
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { useRequest } from '@umijs/max';
|
||||
@@ -102,7 +102,7 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
||||
}
|
||||
}, [visible, activeTab, searchKeyword]);
|
||||
|
||||
const { run: submitItem, loading } = useRequest((newItem) => addStoryItem(newItem), {
|
||||
const { run: submitItem, loading } = useRequest((newItem) => option.includes('edit') ? updateStoryItem(newItem) : addStoryItem(newItem), {
|
||||
manual: true,
|
||||
onSuccess: (data) => {
|
||||
console.log(data);
|
||||
@@ -393,4 +393,4 @@ const AddTimeLineItemModal: React.FC<ModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTimeLineItemModal;
|
||||
export default AddTimeLineItemModal;
|
||||
69
src/pages/story/components/AuthorizeStoryModal.tsx
Normal file
69
src/pages/story/components/AuthorizeStoryModal.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
// 支持授权故事给其他用户,包含查看、新增、增删、管理员权限,查询当前好友列表,选择权限授权
|
||||
import { queryFriendList } from '@/pages/account/center/service';
|
||||
import { useRequest } from '@umijs/max';
|
||||
import { Form, message, Modal, Select } from 'antd';
|
||||
import { StoryType } from '../data';
|
||||
import { values } from 'lodash';
|
||||
import { authorizeStoryPermission } from '../service';
|
||||
|
||||
const AuthorizeStoryModal = ({
|
||||
open,
|
||||
current,
|
||||
handleOk,
|
||||
}: {
|
||||
open: boolean;
|
||||
current: Partial<StoryType> | undefined;
|
||||
handleOk: (flag: boolean) => void;
|
||||
}) => {
|
||||
const { data: friends, loading } = useRequest(() => queryFriendList());
|
||||
const [form] = Form.useForm();
|
||||
const onFinish = async (values: {userId: string, permissionType: number}) => {
|
||||
console.log(current, values);
|
||||
if (!values.userId || !values.permissionType || !current?.instanceId) return;
|
||||
let params = {userId: values.userId, permissionType: values.permissionType, storyInstanceId: current?.instanceId ?? '11'};
|
||||
const res = await authorizeStoryPermission(params);
|
||||
console.log(res);
|
||||
|
||||
if (res.code === 200) {
|
||||
handleOk(true);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal title="授权故事" open={open} onCancel={() => handleOk(false)} onOk={() => onFinish(form.getFieldsValue())}>
|
||||
{/* 请选择需要授权的好友 */}
|
||||
<Form onFinish={onFinish} form={form}>
|
||||
<Form.Item name={'userId'} label="授权对象">
|
||||
<Select
|
||||
// mode="multiple"
|
||||
>
|
||||
{friends?.map((item) => {
|
||||
return (
|
||||
<Select.Option title={item.username} key={item.userId}>
|
||||
{item.username}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="permissionType" label="权限类型">
|
||||
<Select>
|
||||
<Select.Option key="2" title="查看">
|
||||
可编辑
|
||||
</Select.Option>
|
||||
<Select.Option key="3" title="增加">
|
||||
可增加
|
||||
</Select.Option>
|
||||
<Select.Option key="4" title="增删">
|
||||
可增删
|
||||
</Select.Option>
|
||||
<Select.Option key="5" title="管理">
|
||||
可管理
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthorizeStoryModal;
|
||||
@@ -56,23 +56,29 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
// 强制设置裁剪尺寸为 40x40
|
||||
canvas.width = 40;
|
||||
canvas.height = 40;
|
||||
ctx.drawImage(img, 0, 0, 40, 40);
|
||||
if (ctx) {
|
||||
ctx.drawImage(img, 0, 0, 40, 40);
|
||||
|
||||
// 生成 Base64 图像
|
||||
const base64 = canvas.toDataURL('image/png');
|
||||
setSelectedIcon(base64);
|
||||
setIconPreview(base64);
|
||||
setFileList([
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'icon.png',
|
||||
status: 'done',
|
||||
url: base64,
|
||||
originFileObj: new File([dataURLtoBlob(base64)], 'icon.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
// 生成 Base64 图像
|
||||
const base64 = canvas.toDataURL('image/png');
|
||||
setSelectedIcon(base64);
|
||||
setIconPreview(base64);
|
||||
setFileList([
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'icon.png',
|
||||
status: 'done',
|
||||
url: base64,
|
||||
originFileObj: new File([dataURLtoBlob(base64)], 'icon.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
message.error('获取 Canvas 绘图上下文失败');
|
||||
return;
|
||||
}
|
||||
|
||||
};
|
||||
img.onerror = () => {
|
||||
message.error('图像加载失败');
|
||||
@@ -90,7 +96,14 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
// Base64 → Blob 转换工具函数
|
||||
const dataURLtoBlob = (dataurl: string) => {
|
||||
const arr = dataurl.split(',');
|
||||
const mime = arr[0].match(/:(.*?);/)[1];
|
||||
if (!arr || arr.length < 2) {
|
||||
throw new Error('无效的Base64数据');
|
||||
}
|
||||
const mimeMatch = arr[0]?.match(/:(.*?);/);
|
||||
if (!mimeMatch || !mimeMatch[1]) {
|
||||
throw new Error('无法解析MIME类型');
|
||||
}
|
||||
const mime = mimeMatch[1];
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
@@ -248,7 +261,7 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
},
|
||||
]);
|
||||
};
|
||||
img.src = iconPreview;
|
||||
img.src = iconPreview ?? '';
|
||||
}}
|
||||
>
|
||||
<Upload
|
||||
@@ -294,38 +307,10 @@ const OperationModal: FC<OperationModalProps> = (props) => {
|
||||
placeholder="请选择"
|
||||
/>
|
||||
|
||||
<ProFormSelect
|
||||
name="ownerId"
|
||||
label="故事负责人"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择故事负责人',
|
||||
},
|
||||
]}
|
||||
options={[
|
||||
{
|
||||
label: '付晓晓',
|
||||
value: 'xiao',
|
||||
},
|
||||
{
|
||||
label: '周毛毛',
|
||||
value: 'mao',
|
||||
},
|
||||
]}
|
||||
placeholder="请选择管理员"
|
||||
/>
|
||||
|
||||
<ProFormTextArea
|
||||
name="description"
|
||||
label="产品描述"
|
||||
rules={[
|
||||
{
|
||||
message: '请输入至少五个字符的产品描述!',
|
||||
min: 5,
|
||||
},
|
||||
]}
|
||||
placeholder="请输入至少五个字符"
|
||||
label="故事描述"
|
||||
placeholder="可以简单描述当前故事"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -3,12 +3,26 @@ import TimelineImage from '@/components/TimelineImage';
|
||||
import { StoryItem } from '@/pages/story/data';
|
||||
import { DeleteOutlined, DownOutlined, EditOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { useIntl, useRequest } from '@umijs/max';
|
||||
import { Button, Card, message, Popconfirm } from 'antd';
|
||||
import { Button, Card, message, Popconfirm, Tag, Space } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { queryStoryItemImages, removeStoryItem } from '../../service';
|
||||
import TimelineItemDrawer from '../TimelineItemDrawer';
|
||||
import useStyles from './index.style';
|
||||
|
||||
// 格式化时间数组为易读格式
|
||||
const formatTimeArray = (time: string | number[] | undefined): string => {
|
||||
if (!time) return '';
|
||||
|
||||
// 如果是数组格式 [2025, 12, 23, 8, 55, 39]
|
||||
if (Array.isArray(time)) {
|
||||
const [year, month, day, hour, minute, second] = time;
|
||||
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 如果已经是字符串格式,直接返回
|
||||
return String(time);
|
||||
};
|
||||
|
||||
const TimelineItem: React.FC<{
|
||||
item: StoryItem;
|
||||
handleOption: (item: StoryItem, option: 'add' | 'edit' | 'addSubItem' | 'editSubItem') => void;
|
||||
@@ -20,8 +34,11 @@ const TimelineItem: React.FC<{
|
||||
const [showActions, setShowActions] = useState(false);
|
||||
const [subItemsExpanded, setSubItemsExpanded] = useState(false);
|
||||
const [openDetail, setOpenDetail] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { data: imagesList } = useRequest(async () => {
|
||||
return await queryStoryItemImages(item.instanceId);
|
||||
}, {
|
||||
refreshDeps: [item.instanceId]
|
||||
});
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
@@ -52,10 +69,32 @@ const TimelineItem: React.FC<{
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.timelineItem}
|
||||
title={item.title}
|
||||
onMouseEnter={() => setShowActions(true)}
|
||||
onMouseLeave={() => setShowActions(false)}
|
||||
className={`${styles.timelineItem} ${hovered ? styles.timelineItemHover : ''}`}
|
||||
title={
|
||||
<div className={styles.timelineItemTitle}>
|
||||
<div className={styles.timelineItemTitleText}>{item.title}</div>
|
||||
<Space className={styles.timelineItemTags}>
|
||||
{item.createName && (
|
||||
<Tag color="blue" className={styles.creatorTag}>
|
||||
创建: {item.createName}
|
||||
</Tag>
|
||||
)}
|
||||
{item.updateName && item.updateName !== item.createName && (
|
||||
<Tag color="green" className={styles.updaterTag}>
|
||||
更新: {item.updateName}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setShowActions(true);
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowActions(false);
|
||||
setHovered(false);
|
||||
}}
|
||||
extra={
|
||||
<div className={styles.actions}>
|
||||
{showActions && (
|
||||
@@ -100,12 +139,14 @@ const TimelineItem: React.FC<{
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
// onClick={() => onDetail(item)}
|
||||
hoverable
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.date} onClick={() => setOpenDetail(true)}>
|
||||
{item.storyItemTime} {item.location ? `创建于${item.location}` : ''}
|
||||
<Space size="small" className={styles.dateInfo}>
|
||||
<span className={styles.time}>{formatTimeArray(item.storyItemTime)}</span>
|
||||
{item.location && <span className={styles.location}>📍 {item.location}</span>}
|
||||
</Space>
|
||||
</div>
|
||||
<div className={styles.description} onClick={() => setOpenDetail(true)}>
|
||||
{displayedDescription}
|
||||
@@ -125,13 +166,13 @@ const TimelineItem: React.FC<{
|
||||
</div>
|
||||
{imagesList && imagesList.length > 0 && (
|
||||
<>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', marginBottom: '20px' }}>
|
||||
<div className={styles.timelineItemImages}>
|
||||
{imagesList.map((imageInstanceId, index) => (
|
||||
<TimelineImage
|
||||
key={imageInstanceId + index}
|
||||
title={imageInstanceId}
|
||||
imageInstanceId={imageInstanceId}
|
||||
style={{ maxWidth: '100%', height: 'auto' }} // 添加响应式样式
|
||||
className={styles.timelineImage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -156,7 +197,7 @@ const TimelineItem: React.FC<{
|
||||
{item.subItems.map((subItem) => (
|
||||
<div key={subItem.id} className={styles.subItem}>
|
||||
<div className={styles.subItemDate}>
|
||||
{item.storyItemTime} {item.location ? `创建于${item.location}` : ''}
|
||||
{formatTimeArray(item.storyItemTime)} {item.location ? `创建于${item.location}` : ''}
|
||||
</div>
|
||||
<div className={styles.subItemContent}>{subItem.description}</div>
|
||||
</div>
|
||||
@@ -177,4 +218,4 @@ const TimelineItem: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineItem;
|
||||
export default TimelineItem;
|
||||
@@ -9,9 +9,6 @@ const useStyles = createStyles(({ token }) => {
|
||||
borderRadius: token.borderRadius,
|
||||
transition: 'all 0.3s',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
},
|
||||
position: 'relative',
|
||||
padding: '20px',
|
||||
maxWidth: '100%',
|
||||
@@ -24,6 +21,9 @@ const useStyles = createStyles(({ token }) => {
|
||||
padding: '10px',
|
||||
},
|
||||
},
|
||||
timelineItemHover: {
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
},
|
||||
actions: {
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
@@ -31,6 +31,28 @@ const useStyles = createStyles(({ token }) => {
|
||||
height: '24px',
|
||||
width: '120px',
|
||||
},
|
||||
timelineItemTitle: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
timelineItemTitleText: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
timelineItemTags: {
|
||||
flexShrink: 0,
|
||||
marginLeft: '16px',
|
||||
},
|
||||
creatorTag: {
|
||||
fontSize: '12px',
|
||||
},
|
||||
updaterTag: {
|
||||
fontSize: '12px',
|
||||
},
|
||||
content: {
|
||||
padding: '10px 0',
|
||||
},
|
||||
@@ -52,6 +74,20 @@ const useStyles = createStyles(({ token }) => {
|
||||
marginBottom: '10px',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
dateInfo: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
},
|
||||
time: {
|
||||
color: token.colorTextSecondary,
|
||||
},
|
||||
location: {
|
||||
color: token.colorTextTertiary,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
},
|
||||
description: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.6',
|
||||
@@ -97,10 +133,14 @@ const useStyles = createStyles(({ token }) => {
|
||||
color: token.colorText,
|
||||
},
|
||||
timelineItemImages: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', // 减小最小宽度到100px
|
||||
gap: '8px', // 减少间距
|
||||
marginBottom: '20px',
|
||||
maxWidth: '100%', // 确保容器不会超出父元素宽度
|
||||
[`@media (max-width: 768px)`]: {
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(60px, 1fr))', // 在移动设备上减小最小宽度到60px
|
||||
},
|
||||
},
|
||||
timelineImage: {
|
||||
maxWidth: '100%',
|
||||
@@ -116,4 +156,4 @@ const useStyles = createStyles(({ token }) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default useStyles;
|
||||
export default useStyles;
|
||||
@@ -4,9 +4,23 @@ import { StoryItem } from '@/pages/story/data';
|
||||
import { queryStoryItemImages } from '@/pages/story/service';
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { useIntl, useRequest } from '@umijs/max';
|
||||
import { Button, Divider, Drawer, Popconfirm, Space } from 'antd';
|
||||
import { Button, Divider, Drawer, Popconfirm, Space, Tag } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
// 格式化时间数组为易读格式
|
||||
const formatTimeArray = (time: string | number[] | undefined): string => {
|
||||
if (!time) return '';
|
||||
|
||||
// 如果是数组格式 [2025, 12, 23, 8, 55, 39]
|
||||
if (Array.isArray(time)) {
|
||||
const [year, month, day, hour, minute, second] = time;
|
||||
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 如果已经是字符串格式,直接返回
|
||||
return String(time);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
storyItem: StoryItem;
|
||||
open: boolean;
|
||||
@@ -39,9 +53,11 @@ const TimelineItemDrawer: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
// 格式化日期显示
|
||||
const formatDate = (dateString: string) => {
|
||||
const formatDate = (dateString: string | number[] | undefined) => {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
|
||||
const formattedTime = formatTimeArray(dateString);
|
||||
const date = new Date(formattedTime);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
@@ -63,9 +79,23 @@ const TimelineItemDrawer: React.FC<Props> = (props) => {
|
||||
zIndex={1000}
|
||||
title={
|
||||
<div>
|
||||
<h2 style={{ margin: 0 }}>{storyItem.title}</h2>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h2 style={{ margin: 0 }}>{storyItem.title}</h2>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{storyItem.createName && (
|
||||
<Tag color="blue">
|
||||
创建: {storyItem.createName}
|
||||
</Tag>
|
||||
)}
|
||||
{storyItem.updateName && storyItem.updateName !== storyItem.createName && (
|
||||
<Tag color="green">
|
||||
更新: {storyItem.updateName}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: '#888', marginTop: '4px' }}>
|
||||
{storyItem.storyItemTime} {storyItem.location ? `于${storyItem.location}` : ''}
|
||||
{formatTimeArray(storyItem.storyItemTime)} {storyItem.location ? `于${storyItem.location}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -158,9 +188,13 @@ const TimelineItemDrawer: React.FC<Props> = (props) => {
|
||||
<div style={{ color: '#888', marginBottom: '4px' }}>更新时间</div>
|
||||
<div style={{ fontSize: '16px' }}>{formatDate(storyItem.updateTime)}</div>
|
||||
</div>
|
||||
<div style={{ flex: '1', minWidth: '200px' }}>
|
||||
<div style={{ color: '#888', marginBottom: '4px' }}>创建人</div>
|
||||
<div style={{ fontSize: '16px' }}>{storyItem.createName || '系统用户'}</div>
|
||||
</div>
|
||||
<div style={{ flex: '1', minWidth: '200px' }}>
|
||||
<div style={{ color: '#888', marginBottom: '4px' }}>更新人</div>
|
||||
<div style={{ fontSize: '16px' }}>系统用户</div>
|
||||
<div style={{ fontSize: '16px' }}>{storyItem.updateName || storyItem.createName || '系统用户'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,4 +204,4 @@ const TimelineItemDrawer: React.FC<Props> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineItemDrawer;
|
||||
export default TimelineItemDrawer;
|
||||
Reference in New Issue
Block a user