feat: 增加自定义用户信息

并增加用户所在城市信息插件
pull/64/head
moonrailgun 2 years ago
parent b5cc18fbe1
commit 1ad880b948

@ -13,6 +13,7 @@ export interface UserBaseInfo {
discriminator: string; discriminator: string;
avatar: string | null; avatar: string | null;
temporary: boolean; temporary: boolean;
extra?: Record<string, unknown>;
} }
export interface UserLoginInfo extends UserBaseInfo { export interface UserLoginInfo extends UserBaseInfo {
@ -267,6 +268,18 @@ export async function modifyUserField(
return data; return data;
} }
export async function modifyUserExtra(
fieldName: string,
fieldValue: unknown
): Promise<UserBaseInfo> {
const { data } = await request.post('/api/user/updateUserExtra', {
fieldName,
fieldValue,
});
return data;
}
/** /**
* *
*/ */

@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import _set from 'lodash/set';
import type { UserLoginInfo } from '../../model/user'; import type { UserLoginInfo } from '../../model/user';
import type { FriendRequest } from '../../model/friend'; import type { FriendRequest } from '../../model/friend';
@ -29,9 +30,19 @@ const userSlice = createSlice({
if (state.info === null) { if (state.info === null) {
return; return;
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore _set(state.info, [fieldName], fieldValue);
state.info[fieldName] = fieldValue; },
setUserInfoExtra(
state,
action: PayloadAction<{ fieldName: string; fieldValue: any }>
) {
const { fieldName, fieldValue } = action.payload;
if (state.info === null) {
return;
}
_set(state.info, ['extra', fieldName], fieldValue);
}, },
setFriendList(state, action: PayloadAction<string[]>) { setFriendList(state, action: PayloadAction<string[]>) {
state.friends = action.payload; state.friends = action.payload;

@ -0,0 +1,9 @@
{
"label": "用户地理位置",
"name": "com.msgbyte.user.location",
"url": "/plugins/com.msgbyte.user.location/index.js",
"version": "0.0.0",
"author": "moonrailgun",
"description": "为用户信息增加地理位置记录",
"requireRestart": true
}

@ -0,0 +1,16 @@
{
"name": "@plugins/com.msgbyte.user.location",
"main": "src/index.tsx",
"version": "0.0.0",
"description": "为用户信息增加地理位置记录",
"private": true,
"scripts": {
"sync:declaration": "tailchat declaration github"
},
"dependencies": {},
"devDependencies": {
"@types/styled-components": "^5.1.26",
"react": "18.2.0",
"styled-components": "^5.3.6"
}
}

@ -0,0 +1,6 @@
import { regUserExtraInfo, localTrans } from '@capital/common';
regUserExtraInfo({
name: 'location',
label: localTrans({ 'zh-CN': '所在城市', 'en-US': 'City' }),
});

@ -0,0 +1,7 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"importsNotUsedAsValues": "error"
}
}

@ -0,0 +1,2 @@
declare module '@capital/common';
declare module '@capital/component';

@ -75,5 +75,14 @@
"author": "moonrailgun", "author": "moonrailgun",
"description": "快捷打开 files.fm 以支持传输文件", "description": "快捷打开 files.fm 以支持传输文件",
"requireRestart": true "requireRestart": true
},
{
"label": "用户地理位置",
"name": "com.msgbyte.user.location",
"url": "/plugins/com.msgbyte.user.location/index.js",
"version": "0.0.0",
"author": "moonrailgun",
"description": "为用户信息增加地理位置记录",
"requireRestart": true
} }
] ]

@ -99,15 +99,28 @@ const FullModalFieldEditor: React.FC<FullModalFieldProps> = React.memo(
<div className="ml-2"> <div className="ml-2">
{!isEditing ? ( {!isEditing ? (
<DelayTip title={t('编辑')}> <DelayTip title={t('编辑')}>
<IconBtn icon="mdi:square-edit-outline" onClick={handleEditing} /> <IconBtn
size="small"
icon="mdi:square-edit-outline"
onClick={handleEditing}
/>
</DelayTip> </DelayTip>
) : ( ) : (
<Space> <Space>
<DelayTip title={t('取消')}> <DelayTip title={t('取消')}>
<IconBtn icon="mdi:close" onClick={handleEditing} /> <IconBtn
size="small"
icon="mdi:close"
onClick={handleEditing}
/>
</DelayTip> </DelayTip>
<DelayTip title={t('保存变更')}> <DelayTip title={t('保存变更')}>
<IconBtn type="primary" icon="mdi:check" onClick={handleSave} /> <IconBtn
type="primary"
size="small"
icon="mdi:check"
onClick={handleSave}
/>
</DelayTip> </DelayTip>
</Space> </Space>
)} )}

@ -1,19 +1,21 @@
import { Avatar } from '@/components/Avatar';
import { AvatarUploader } from '@/components/AvatarUploader'; import { AvatarUploader } from '@/components/AvatarUploader';
import { import {
DefaultFullModalInputEditorRender, DefaultFullModalInputEditorRender,
FullModalField, FullModalField,
} from '@/components/FullModal/Field'; } from '@/components/FullModal/Field';
import { openModal } from '@/components/Modal'; import { openModal } from '@/components/Modal';
import { closeModal } from '@/plugin/common'; import { closeModal, pluginUserExtraInfo } from '@/plugin/common';
import { getGlobalSocket } from '@/utils/global-state-helper'; import { getGlobalSocket } from '@/utils/global-state-helper';
import { setUserJWT } from '@/utils/jwt-helper'; import { setUserJWT } from '@/utils/jwt-helper';
import { setGlobalUserLoginInfo } from '@/utils/user-helper'; import { setGlobalUserLoginInfo } from '@/utils/user-helper';
import { Button, Divider, Typography } from 'antd'; import { Button, Divider, Typography } from 'antd';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { Avatar } from 'tailchat-design';
import { import {
model,
modifyUserField, modifyUserField,
showSuccessToasts,
showToasts, showToasts,
t, t,
UploadFileResult, UploadFileResult,
@ -28,6 +30,7 @@ export const SettingsAccount: React.FC = React.memo(() => {
const userInfo = useUserInfo(); const userInfo = useUserInfo();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const userExtra = userInfo?.extra ?? {};
const [, handleUserAvatarChange] = useAsyncRequest( const [, handleUserAvatarChange] = useAsyncRequest(
async (fileInfo: UploadFileResult) => { async (fileInfo: UploadFileResult) => {
@ -57,6 +60,20 @@ export const SettingsAccount: React.FC = React.memo(() => {
[] []
); );
const [, handleUpdateExtraInfo] = useAsyncRequest(
async (fieldName: string, fieldValue: unknown) => {
await model.user.modifyUserExtra(fieldName, fieldValue);
dispatch(
userActions.setUserInfoExtra({
fieldName,
fieldValue,
})
);
showSuccessToasts(t('修改成功'));
},
[]
);
const handleUpdatePassword = useCallback(() => { const handleUpdatePassword = useCallback(() => {
const key = openModal(<ModifyPassword onSuccess={() => closeModal(key)} />); const key = openModal(<ModifyPassword onSuccess={() => closeModal(key)} />);
}, []); }, []);
@ -93,6 +110,30 @@ export const SettingsAccount: React.FC = React.memo(() => {
renderEditor={DefaultFullModalInputEditorRender} renderEditor={DefaultFullModalInputEditorRender}
onSave={handleUpdateNickName} onSave={handleUpdateNickName}
/> />
{pluginUserExtraInfo.map((item, i) => {
if (item.component && item.component.editor) {
const Component = item.component.editor;
return (
<Component
key={item.name + i}
value={userExtra[item.name]}
onSave={(val) => handleUpdateExtraInfo(item.name, val)}
/>
);
}
return (
<FullModalField
key={item.name + i}
title={item.label}
value={userExtra[item.name] ? String(userExtra[item.name]) : ''}
editable={true}
renderEditor={DefaultFullModalInputEditorRender}
onSave={(val) => handleUpdateExtraInfo(item.name, val)}
/>
);
})}
</div> </div>
</div> </div>

@ -1,3 +1,4 @@
import { pluginUserExtraInfo } from '@/plugin/common';
import { fetchImagePrimaryColor } from '@/utils/image-helper'; import { fetchImagePrimaryColor } from '@/utils/image-helper';
import { Tag } from 'antd'; import { Tag } from 'antd';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
@ -8,6 +9,7 @@ export const GroupUserPopover: React.FC<{
userInfo: UserBaseInfo; userInfo: UserBaseInfo;
}> = React.memo((props) => { }> = React.memo((props) => {
const { userInfo } = props; const { userInfo } = props;
const userExtra = userInfo.extra ?? {};
useEffect(() => { useEffect(() => {
if (userInfo.avatar) { if (userInfo.avatar) {
@ -28,6 +30,24 @@ export const GroupUserPopover: React.FC<{
<div> <div>
{userInfo.temporary && <Tag color="processing">{t('游客')}</Tag>} {userInfo.temporary && <Tag color="processing">{t('游客')}</Tag>}
</div> </div>
<div className="pt-2">
{pluginUserExtraInfo.map((item, i) => {
const Component = item.component?.render;
return (
<div key={item.name + i} className="flex">
<div className="w-1/4 text-gray-500">{item.label}:</div>
<div className="w-3/4">
{Component ? (
<Component value={userExtra[item.name]} />
) : (
String(userExtra[item.name])
)}
</div>
</div>
);
})}
</div>
</UserProfileContainer> </UserProfileContainer>
</div> </div>
); );

@ -49,6 +49,7 @@ export const builtinPlugins: PluginManifest[] = _compact([
description: '为应用首次打开介绍应用的能力', description: '为应用首次打开介绍应用的能力',
requireRestart: true, requireRestart: true,
}, },
// isOffical
isOffical && { isOffical && {
label: 'Posthog', label: 'Posthog',
name: 'com.msgbyte.posthog', name: 'com.msgbyte.posthog',
@ -69,4 +70,13 @@ export const builtinPlugins: PluginManifest[] = _compact([
description: 'Sentry 错误处理', description: 'Sentry 错误处理',
requireRestart: true, requireRestart: true,
}, },
isOffical && {
label: '用户地理位置',
name: 'com.msgbyte.user.location',
url: '/plugins/com.msgbyte.user.location/index.js',
version: '0.0.0',
author: 'moonrailgun',
description: '为用户信息增加地理位置记录',
requireRestart: true,
},
]); ]);

@ -230,3 +230,27 @@ export const [
pluginGroupTextPanelExtraMenus, pluginGroupTextPanelExtraMenus,
regPluginGroupTextPanelExtraMenu, regPluginGroupTextPanelExtraMenu,
] = buildRegList<PluginPanelMenu>(); ] = buildRegList<PluginPanelMenu>();
interface PluginUserExtraInfo {
name: string;
label: string;
/**
*
*
*/
component?: {
render?: React.ComponentType<{
value: unknown;
}>;
editor?: React.ComponentType<{
value: unknown;
onSave: (val: unknown) => void;
}>;
};
}
/**
* ()
*/
export const [pluginUserExtraInfo, regUserExtraInfo] =
buildRegList<PluginUserExtraInfo>();

Loading…
Cancel
Save