refactor: add admin resource i18n support

pull/90/head
moonrailgun 2 years ago
parent 8066330d60
commit 4c168eb81f

@ -15,7 +15,7 @@ import { Dashboard } from './dashboard';
import { Route } from 'react-router-dom';
import { TailchatNetwork } from './routes/network';
import { TailchatLayout } from './layout';
import { i18nProvider } from './i18n';
import { i18nProvider } from './i18n/index';
import { httpClient } from './request';
import { SocketIOAdmin } from './routes/socketio';
@ -37,31 +37,22 @@ export const App = () => (
i18nProvider={i18nProvider}
requireAuth={true}
>
<Resource
icon={PersonIcon}
name="users"
options={{ label: '用户管理' }}
list={UserList}
show={UserShow}
/>
<Resource icon={PersonIcon} name="users" list={UserList} show={UserShow} />
<Resource
icon={MessageIcon}
name="messages"
options={{ label: '聊天信息' }}
list={MessageList}
show={ShowGuesser}
/>
<Resource
icon={GroupIcon}
name="groups"
options={{ label: '群组' }}
list={GroupList}
show={GroupShow}
/>
<Resource
icon={AttachFileIcon}
name="file"
options={{ label: '文件' }}
list={FileList}
show={ShowGuesser}
/>

@ -18,7 +18,7 @@ export const authProvider: AuthProvider = {
localStorage.setItem(authStorageKey, JSON.stringify(auth));
})
.catch(() => {
throw new Error('登录失败');
throw new Error('Login Failed');
});
},
logout: () => {

@ -1,205 +0,0 @@
import { TranslationMessages } from 'react-admin';
import _merge from 'lodash/merge';
import defaultEnglishMessages from 'ra-language-english';
import polyglotI18nProvider from 'ra-i18n-polyglot';
const chineseResources = {
resources: {
users: {
fields: {
id: '用户ID',
email: '邮箱',
avatar: '头像',
username: '用户名',
password: '密码',
nickname: '昵称',
discriminator: '标识符',
temporary: '是否游客',
type: '用户类型',
settings: '用户设置',
createdAt: '创建时间',
},
},
},
};
const chineseMessages: TranslationMessages = _merge(
defaultEnglishMessages,
{
ra: {
action: {
add_filter: '增加检索',
add: '增加',
back: '回退',
bulk_actions: '选中%{smart_count}项',
cancel: '取消',
clear_input_value: '清空输入',
clone: '克隆',
confirm: '确认',
create: '新建',
create_item: '新建 %{item}',
delete: '删除',
edit: '编辑',
export: '导出',
list: '列表',
refresh: '刷新',
remove_filter: '移除检索',
remove: '删除',
save: '保存',
search: '检索',
select_all: '选中全部',
select_row: '选中这行',
show: '查看',
sort: '排序',
undo: '撤销',
unselect: '反选',
expand: '展开',
close: '关闭',
open_menu: '打开菜单',
close_menu: '关闭菜单',
update: '更新',
move_up: '上移',
move_down: '下移',
open: '打开',
toggle_theme: '切换主题',
},
boolean: {
true: '是',
false: '否',
null: '',
},
page: {
create: '新建 %{name}',
dashboard: '概览',
edit: '%{name} #%{id}',
error: '出现错误',
list: '%{name} 列表',
loading: '加载中',
not_found: '未发现',
show: '%{name} #%{id}',
empty: '无 %{name} ',
invite: '要增加吗?',
},
input: {
file: {
upload_several: '将文件集合拖拽到这里, 或点击这里选择文件集合.',
upload_single: '将文件拖拽到这里, 或点击这里选择文件.',
},
image: {
upload_several:
'将图片文件集合拖拽到这里, 或点击这里选择图片文件集合.',
upload_single: '将图片文件拖拽到这里, 或点击这里选择图片文件.',
},
references: {
all_missing: '未找到参考数据.',
many_missing: '至少有一条参考数据不再可用.',
single_missing: '关联的参考数据不再可用.',
},
password: {
toggle_visible: '隐藏密码',
toggle_hidden: '显示密码',
},
},
message: {
about: '关于',
are_you_sure: '您确定操作?',
bulk_delete_title: '删除 %{name} |||| 删除 %{smart_count}项 %{name} ',
bulk_delete_content:
'您确定要删除 %{name}? |||| 您确定要删除 %{smart_count} 项?',
bulk_update_content:
'你确定要更新 %{name}? |||| 你确定想更新 %{smart_count} 项?',
bulk_update_title:
'更新 %{name} |||| 您确定更新 %{smart_count} %{name}',
delete_content: '您确定要删除该条目?',
delete_title: '删除 %{name} #%{id}',
details: '详情',
error: '客户端错误导致请求未完成.',
invalid_form: '表单输入无效. 请检查错误提示',
loading: '正在加载页面, 请稍候',
no: '否',
not_found: '您输入了错误的URL或者错误的链接.',
yes: '是',
unsaved_changes: '修改未保存. 放弃修改吗?',
},
navigation: {
no_results: '结果为空',
no_more_results: '页码 %{page} 超出边界. 试试上一页.',
page_out_of_boundaries: '页码 %{page} 超出边界',
page_out_from_end: '已到最末页',
page_out_from_begin: '已到最前页',
page_range_info: '%{offsetBegin}-%{offsetEnd} / %{total}',
partial_page_range_info:
'%{offsetBegin}-%{offsetEnd} of more than %{offsetEnd}',
current_page: '页码 %{page}',
page: '跳转到 %{page}',
prev: '向前',
first: '第一页',
last: '最后一页',
next: '向后',
previous: '先前第一页',
page_rows_per_page: '每页行数:',
skip_nav: '跳到内容',
},
sort: {
sort_by: '按 %{field} %{order}',
ASC: '升序',
DESC: '降序',
},
auth: {
auth_check_error: '请登录以继续',
user_menu: '设置',
username: '用户名',
password: '密码',
sign_in: '登录',
sign_in_error: '验证失败, 请重试',
logout: '退出',
},
notification: {
updated: '条目已更新 |||| %{smart_count} 项条目已更新',
created: '条目已新建',
deleted: '条目已删除 |||| %{smart_count} 项条目已删除',
bad_item: '不正确的条目',
item_doesnt_exist: '条目不存在',
http_error: '与服务通信出错',
canceled: '取消动作',
data_provider_error: 'dataProvider错误. 请检查console的详细信息.',
i18n_error: '无法加载指定语言包',
logged_out: '会话失效, 请重连.',
not_authorized: '您无权访问此资源.',
},
validation: {
required: '必填',
minLength: '必须不少于 %{min} 个字符',
maxLength: '必须不多于 %{max} 个字符',
minValue: '必须不小于 %{min}',
maxValue: '必须不大于 %{max}',
number: '必须为数字',
email: '必须是有效的邮箱',
oneOf: '必须为: %{options}其中一项',
regex: '必须符合指定的格式 (regexp): %{pattern}',
},
saved_queries: {
label: '保存查询',
query_name: '搜索名称',
new_label: '保存当前的查询...',
new_dialog_title: '将当前查询另存为',
remove_label: '删除保存的查询',
remove_label_with_name: '删除查询 "%{name}"',
remove_dialog_title: '是否删除保存的查询?',
remove_message: '确实要从保存的查询列表中删除该项吗?',
help: '筛选列表并保存此查询以备将来使用',
},
},
},
chineseResources
);
const englishMessages = _merge(defaultEnglishMessages, chineseResources);
export const i18nProvider = polyglotI18nProvider((locale: string) => {
if (locale === 'ch') {
return chineseMessages;
} else {
return englishMessages;
}
}, 'ch');

@ -0,0 +1,164 @@
export const defaultChineseMessages = {
ra: {
action: {
add_filter: '增加检索',
add: '增加',
back: '回退',
bulk_actions: '选中%{smart_count}项',
cancel: '取消',
clear_input_value: '清空输入',
clone: '克隆',
confirm: '确认',
create: '新建',
create_item: '新建 %{item}',
delete: '删除',
edit: '编辑',
export: '导出',
list: '列表',
refresh: '刷新',
remove_filter: '移除检索',
remove: '删除',
save: '保存',
search: '检索',
select_all: '选中全部',
select_row: '选中这行',
show: '查看',
sort: '排序',
undo: '撤销',
unselect: '反选',
expand: '展开',
close: '关闭',
open_menu: '打开菜单',
close_menu: '关闭菜单',
update: '更新',
move_up: '上移',
move_down: '下移',
open: '打开',
toggle_theme: '切换主题',
},
boolean: {
true: '是',
false: '否',
null: '',
},
page: {
create: '新建 %{name}',
dashboard: '概览',
edit: '%{name} #%{id}',
error: '出现错误',
list: '%{name} 列表',
loading: '加载中',
not_found: '未发现',
show: '%{name} #%{id}',
empty: '无 %{name} ',
invite: '要增加吗?',
},
input: {
file: {
upload_several: '将文件集合拖拽到这里, 或点击这里选择文件集合.',
upload_single: '将文件拖拽到这里, 或点击这里选择文件.',
},
image: {
upload_several: '将图片文件集合拖拽到这里, 或点击这里选择图片文件集合.',
upload_single: '将图片文件拖拽到这里, 或点击这里选择图片文件.',
},
references: {
all_missing: '未找到参考数据.',
many_missing: '至少有一条参考数据不再可用.',
single_missing: '关联的参考数据不再可用.',
},
password: {
toggle_visible: '隐藏密码',
toggle_hidden: '显示密码',
},
},
message: {
about: '关于',
are_you_sure: '您确定操作?',
bulk_delete_title: '删除 %{name} |||| 删除 %{smart_count}项 %{name} ',
bulk_delete_content:
'您确定要删除 %{name}? |||| 您确定要删除 %{smart_count} 项?',
bulk_update_content:
'你确定要更新 %{name}? |||| 你确定想更新 %{smart_count} 项?',
bulk_update_title: '更新 %{name} |||| 您确定更新 %{smart_count} %{name}',
delete_content: '您确定要删除该条目?',
delete_title: '删除 %{name} #%{id}',
details: '详情',
error: '客户端错误导致请求未完成.',
invalid_form: '表单输入无效. 请检查错误提示',
loading: '正在加载页面, 请稍候',
no: '否',
not_found: '您输入了错误的URL或者错误的链接.',
yes: '是',
unsaved_changes: '修改未保存. 放弃修改吗?',
},
navigation: {
no_results: '结果为空',
no_more_results: '页码 %{page} 超出边界. 试试上一页.',
page_out_of_boundaries: '页码 %{page} 超出边界',
page_out_from_end: '已到最末页',
page_out_from_begin: '已到最前页',
page_range_info: '%{offsetBegin}-%{offsetEnd} / %{total}',
partial_page_range_info:
'%{offsetBegin}-%{offsetEnd} of more than %{offsetEnd}',
current_page: '页码 %{page}',
page: '跳转到 %{page}',
prev: '向前',
first: '第一页',
last: '最后一页',
next: '向后',
previous: '先前第一页',
page_rows_per_page: '每页行数:',
skip_nav: '跳到内容',
},
sort: {
sort_by: '按 %{field} %{order}',
ASC: '升序',
DESC: '降序',
},
auth: {
auth_check_error: '请登录以继续',
user_menu: '设置',
username: '用户名',
password: '密码',
sign_in: '登录',
sign_in_error: '验证失败, 请重试',
logout: '退出',
},
notification: {
updated: '条目已更新 |||| %{smart_count} 项条目已更新',
created: '条目已新建',
deleted: '条目已删除 |||| %{smart_count} 项条目已删除',
bad_item: '不正确的条目',
item_doesnt_exist: '条目不存在',
http_error: '与服务通信出错',
canceled: '取消动作',
data_provider_error: 'dataProvider错误. 请检查console的详细信息.',
i18n_error: '无法加载指定语言包',
logged_out: '会话失效, 请重连.',
not_authorized: '您无权访问此资源.',
},
validation: {
required: '必填',
minLength: '必须不少于 %{min} 个字符',
maxLength: '必须不多于 %{max} 个字符',
minValue: '必须不小于 %{min}',
maxValue: '必须不大于 %{max}',
number: '必须为数字',
email: '必须是有效的邮箱',
oneOf: '必须为: %{options}其中一项',
regex: '必须符合指定的格式 (regexp): %{pattern}',
},
saved_queries: {
label: '保存查询',
query_name: '搜索名称',
new_label: '保存当前的查询...',
new_dialog_title: '将当前查询另存为',
remove_label: '删除保存的查询',
remove_label_with_name: '删除查询 "%{name}"',
remove_dialog_title: '是否删除保存的查询?',
remove_message: '确实要从保存的查询列表中删除该项吗?',
help: '筛选列表并保存此查询以备将来使用',
},
},
};

@ -0,0 +1,62 @@
export const englishCustom = {
custom: {
common: {
summary: 'Summary',
panel: 'Panel',
name: 'Name',
permission: 'Permission',
},
users: {
search: 'Search nickname or email',
resetPassword: 'Reset Password',
resetPasswordTip:
'After resetting the password, the password becomes: 123456789, please change the password in time',
},
messages: {
search: 'Search Message Content',
},
groups: {
noAvatar: 'No Avatar',
'panels.name': 'Panel Name',
'panels.type': 'Panel Type',
'panels.provider': 'Panel Provider',
'panels.pluginPanelName': 'Panel Name',
'panels.meta': 'Panel Meta',
'panels.parentId': 'Panel Parent',
textPanel: 'Text Panel',
groupPanel: 'Panel Group',
pluginPanel: 'Plugin Panel',
},
},
};
export const chineseCustom = {
custom: {
common: {
summary: '概述',
panel: '面板',
name: '名称',
permission: '权限',
},
users: {
search: '搜索昵称或邮箱',
resetPassword: '重置密码',
resetPasswordTip: '重置密码后密码变为: 123456789, 请及时修改密码',
},
messages: {
search: '搜索消息内容',
},
groups: {
noAvatar: '无头像',
'panels.name': '面板名',
'panels.type': '面板类型',
'panels.provider': '面板供应插件',
'panels.pluginPanelName': '插件面板名',
'panels.meta': '面板元信息',
'panels.parentId': '面板父级',
textPanel: '文本频道',
groupPanel: '面板分组',
pluginPanel: '插件面板',
},
},
};

@ -0,0 +1,37 @@
import { TranslationMessages } from 'react-admin';
import _merge from 'lodash/merge';
import defaultEnglishMessages from 'ra-language-english';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import { chineseResources, englishResources } from './resources';
import { chineseCustom, englishCustom } from './custom';
import { defaultChineseMessages } from './builtin';
const chineseMessages: TranslationMessages = _merge(
{},
defaultEnglishMessages,
defaultChineseMessages,
chineseResources,
chineseCustom
);
const englishMessages = _merge(
{},
defaultEnglishMessages,
englishResources,
englishCustom
);
export const i18nProvider = polyglotI18nProvider(
(locale: string) => {
if (locale === 'ch') {
return chineseMessages;
} else {
return englishMessages;
}
},
'en',
[
{ locale: 'en', name: 'English' },
{ locale: 'ch', name: '简体中文' },
]
);

@ -0,0 +1,123 @@
export const englishResources = {
resources: {
users: {
name: 'User',
fields: {
id: 'ID',
email: 'Email',
avatar: 'Avatar',
username: 'Username',
password: 'Password',
nickname: 'Nick Name',
discriminator: 'Discriminator',
temporary: 'is Template User',
type: 'User Type',
settings: 'User Settings',
createdAt: 'Create Time',
},
},
messages: {
name: 'Messages',
fields: {
content: 'Content',
author: 'Author',
groupId: 'Group ID',
converseId: 'Converse ID',
hasRecall: 'Recall',
reactions: 'Reactions',
createdAt: 'Create Time',
},
},
groups: {
name: 'Group',
fields: {
id: 'Group ID',
name: 'Group Name',
avatar: 'Avatar',
owner: 'Owner',
members: 'Member List',
'members.length': 'Member count',
'panels.length': 'Panel count',
roles: 'Roles',
config: 'Config',
panels: 'Group Panels',
fallbackPermissions: 'Default Permission',
createdAt: 'Create Time',
updatedAt: 'Update Time',
},
},
file: {
name: 'File',
fields: {
objectName: 'Object Name',
url: 'Path',
size: 'Size',
'metaData.content-type': 'Type',
userId: 'Storage User',
createdAt: 'Create Time',
},
},
},
};
export const chineseResources = {
resources: {
users: {
name: '用户管理',
fields: {
id: '用户ID',
email: '邮箱',
avatar: '头像',
username: '用户名',
password: '密码',
nickname: '昵称',
discriminator: '标识符',
temporary: '是否游客',
type: '用户类型',
settings: '用户设置',
createdAt: '创建时间',
},
},
messages: {
name: '消息管理',
fields: {
content: '内容',
author: '作者',
groupId: '群组ID',
converseId: '会话ID',
hasRecall: '撤回',
reactions: '消息反应',
createdAt: '创建时间',
},
},
groups: {
name: '群组管理',
fields: {
id: '群组ID',
name: '群组名称',
avatar: '头像',
owner: '管理员',
members: '成员列表',
'members.length': '成员数量',
'panels.length': '面板数量',
roles: '角色',
config: '配置信息',
panels: '群组面板',
fallbackPermissions: '默认权限',
createdAt: '创建时间',
updatedAt: '更新时间',
},
},
file: {
name: '文件管理',
fields: {
objectName: '对象存储名',
url: '文件路径',
size: '文件大小',
'metaData.content-type': '文件类型',
userId: '存储用户',
createdAt: '创建时间',
},
},
},
};

@ -7,29 +7,34 @@ import {
ReferenceField,
TextField,
SearchInput,
useTranslate,
} from 'react-admin';
export const MessageList: React.FC = () => (
<List
filters={[
<SearchInput
key="search"
source="q"
alwaysOn
placeholder="搜索消息内容"
/>,
]}
>
<Datagrid rowClick="show">
<TextField source="id" label="ID" sortable={true} sortByOrder="DESC" />
<TextField source="content" label="内容" />
<TextField source="author" label="作者" />
<ReferenceField source="groupId" reference="groups" label="群组ID" />
<TextField source="converseId" label="会话ID" />
<BooleanField source="hasRecall" label="撤回" />
<TextField source="reactions" label="消息反应" />
<DateField source="createdAt" label="创建时间" />
</Datagrid>
</List>
);
export const MessageList: React.FC = () => {
const translate = useTranslate();
return (
<List
filters={[
<SearchInput
key="search"
source="q"
alwaysOn
placeholder={translate('custom.users.search')}
/>,
]}
>
<Datagrid rowClick="show">
<TextField source="id" sortable={true} sortByOrder="DESC" />
<TextField source="content" />
<TextField source="author" />
<ReferenceField source="groupId" reference="groups" />
<TextField source="converseId" />
<BooleanField source="hasRecall" />
<TextField source="reactions" />
<DateField source="createdAt" />
</Datagrid>
</List>
);
};
MessageList.displayName = 'MessageList';

@ -12,16 +12,16 @@ import { FilesizeField } from '../components/FilesizeField';
export const FileList: React.FC = () => (
<List>
<Datagrid>
<TextField source="objectName" label="对象存储名" />
<UrlField source="url" target="__blank" label="文件路径" />
<TextField source="objectName" />
<UrlField source="url" target="__blank" />
<FilesizeField source="size" noWrap={true} />
<TextField source="metaData.content-type" label="文件类型" />
<TextField source="metaData.content-type" />
<TextField source="etag" />
<ReferenceField source="userId" reference="users" label="存储用户">
<ReferenceField source="userId" reference="users">
<TextField source="nickname" />
(<TextField source="email" />)
</ReferenceField>
<DateField source="createdAt" label="创建时间" />
<DateField source="createdAt" />
</Datagrid>
</List>
);

@ -9,13 +9,10 @@ import {
SingleFieldList,
ChipField,
Show,
SimpleShowLayout,
NumberField,
ReferenceField,
BooleanField,
SelectField,
TabbedShowLayout,
ImageField,
useTranslate,
} from 'react-admin';
import React from 'react';
import { Box } from '@mui/material';
@ -28,23 +25,18 @@ const PostListActionToolbar = ({ children, ...props }) => (
export const GroupList: React.FC = () => (
<List filters={[<SearchInput key="search" source="q" alwaysOn />]}>
<Datagrid>
<TextField
source="id"
label="群组ID"
sortable={true}
sortByOrder="DESC"
/>
<TextField source="name" label="群组名称" />
<TextField source="owner" label="管理员" />
<TextField source="members.length" label="成员数量" />
<TextField source="panels.length" label="面板数量" />
<ArrayField source="roles" label="角色">
<TextField source="id" sortable={true} sortByOrder="DESC" />
<TextField source="name" />
<TextField source="owner" />
<TextField source="members.length" />
<TextField source="panels.length" />
<ArrayField source="roles">
<SingleFieldList>
<ChipField source="name" />
</SingleFieldList>
</ArrayField>
<TextField source="fallbackPermissions" label="默认权限" />
<DateField source="createdAt" label="创建时间" />
<TextField source="fallbackPermissions" />
<DateField source="createdAt" />
<PostListActionToolbar>
<ShowButton />
</PostListActionToolbar>
@ -53,64 +45,96 @@ export const GroupList: React.FC = () => (
);
GroupList.displayName = 'GroupList';
export const GroupShow: React.FC = () => (
<Show>
<TabbedShowLayout>
<TabbedShowLayout.Tab label="概述">
<TextField source="id" />
<ImageField source="avatar" label="头像" emptyText="(无头像)" />
<TextField source="name" label="群组名" />
<UserField source="owner" label="所有人" />
export const GroupShow: React.FC = () => {
const translate = useTranslate();
<DateField source="createdAt" label="创建时间" />
<DateField source="updatedAt" label="更新时间" />
<TextField source="fallbackPermissions" label="默认权限" />
<TextField source="config" label="配置信息" />
</TabbedShowLayout.Tab>
return (
<Show>
<TabbedShowLayout>
<TabbedShowLayout.Tab label={translate('custom.common.summary')}>
<TextField source="id" />
<ImageField
source="avatar"
emptyText={`(${translate('custom.groups.noAvatar')})`}
/>
<TextField source="name" />
<UserField source="owner" />
{/* 面板 */}
<TabbedShowLayout.Tab label="面板">
<ArrayField source="panels" label="群组面板">
<Datagrid>
<TextField source="id" />
<TextField source="name" label="面板名" />
<SelectField
source="type"
choices={[
{ id: 0, name: '文本频道' },
{ id: 1, name: '面板分组' },
{ id: 2, name: '插件面板' },
]}
label={'面板类型'}
/>
<TextField source="provider" label="面板供应插件" />
<TextField source="pluginPanelName" label="插件面板名" />
<TextField source="meta" label="面板元信息" />
<TextField source="parentId" label="面板父级" />
</Datagrid>
</ArrayField>
</TabbedShowLayout.Tab>
<DateField source="createdAt" />
<DateField source="updatedAt" />
<TextField source="fallbackPermissions" />
<TextField source="config" />
</TabbedShowLayout.Tab>
{/* 身份组 */}
<TabbedShowLayout.Tab label="身份组">
<ArrayField source="roles" label="身份组">
<Datagrid>
<TextField source="name" label="名称" />
<TextField source="permission" label="权限" />
</Datagrid>
</ArrayField>
</TabbedShowLayout.Tab>
{/* 面板 */}
<TabbedShowLayout.Tab label={translate('custom.common.panel')}>
<ArrayField source="panels">
<Datagrid>
<TextField source="id" />
<TextField
source="name"
label={translate('custom.groups.panels.name')}
/>
<SelectField
source="type"
choices={[
{ id: 0, name: translate('custom.groups.textPanel') },
{ id: 1, name: translate('custom.groups.groupPanel') },
{ id: 2, name: translate('custom.groups.pluginPanel') },
]}
label={translate('custom.groups.panels.type')}
/>
<TextField
source="provider"
label={translate('custom.groups.panels.provider')}
/>
<TextField
source="pluginPanelName"
label={translate('custom.groups.panels.name')}
/>
<TextField
source="meta"
label={translate('custom.groups.panels.meta')}
/>
<TextField
source="parentId"
label={translate('custom.groups.panels.parentId')}
/>
</Datagrid>
</ArrayField>
</TabbedShowLayout.Tab>
{/* 成员列表 */}
<TabbedShowLayout.Tab label="成员列表">
<ArrayField source="members" label="成员列表">
<Datagrid>
<UserField source="userId" label="成员" />
<TextField source="roles" label="角色" />
</Datagrid>
</ArrayField>
</TabbedShowLayout.Tab>
</TabbedShowLayout>
</Show>
);
{/* 身份组 */}
<TabbedShowLayout.Tab
label={translate('resources.groups.fields.roles')}
>
<ArrayField source="roles">
<Datagrid>
<TextField
source="name"
label={translate('custom.common.name')}
/>
<TextField
source="permission"
label={translate('custom.common.permission')}
/>
</Datagrid>
</ArrayField>
</TabbedShowLayout.Tab>
{/* 成员列表 */}
<TabbedShowLayout.Tab
label={translate('resources.groups.fields.members')}
>
<ArrayField source="members">
<Datagrid>
<UserField source="userId" />
<TextField source="roles" />
</Datagrid>
</ArrayField>
</TabbedShowLayout.Tab>
</TabbedShowLayout>
</Show>
);
};
GroupShow.displayName = 'GroupShow';

@ -13,6 +13,7 @@ import {
TopToolbar,
useUpdate,
useShowContext,
useTranslate,
} from 'react-admin';
import React from 'react';
import { Box } from '@mui/material';
@ -23,41 +24,46 @@ const PostListActionToolbar = ({ children, ...props }) => (
<Box sx={{ alignItems: 'center', display: 'flex' }}>{children}</Box>
);
export const UserList: React.FC = () => (
<List
filters={[
<SearchInput
key="search"
source="q"
alwaysOn
placeholder="搜索昵称或邮箱"
/>,
]}
>
<Datagrid>
<TextField source="id" sortByOrder="DESC" />
<EmailField source="email" />
<TextField source="nickname" />
<TextField source="discriminator" />
<BooleanField source="temporary" />
<ImageField
sx={{ '.RaImageField-image': { height: 40, width: 40 } }}
source="avatar"
/>
<TextField source="type" />
<TextField source="settings" />
<DateField source="createdAt" />
<PostListActionToolbar>
<ShowButton />
</PostListActionToolbar>
</Datagrid>
</List>
);
export const UserList: React.FC = () => {
const translate = useTranslate();
return (
<List
filters={[
<SearchInput
key="search"
source="q"
alwaysOn
placeholder={translate('custom.users.search')}
/>,
]}
>
<Datagrid>
<TextField source="id" sortByOrder="DESC" />
<EmailField source="email" />
<TextField source="nickname" />
<TextField source="discriminator" />
<BooleanField source="temporary" />
<ImageField
sx={{ '.RaImageField-image': { height: 40, width: 40 } }}
source="avatar"
/>
<TextField source="type" />
<TextField source="settings" />
<DateField source="createdAt" />
<PostListActionToolbar>
<ShowButton />
</PostListActionToolbar>
</Datagrid>
</List>
);
};
UserList.displayName = 'UserList';
const UserShowActions: React.FC = () => {
const [update] = useUpdate();
const { record, refetch, resource } = useShowContext();
const translate = useTranslate();
return (
<TopToolbar>
@ -65,8 +71,8 @@ const UserShowActions: React.FC = () => {
<ButtonWithConfirm
component={DangerButton}
label="重置密码"
confirmContent="重置密码后密码变为: 123456789, 请及时修改密码"
label={translate('custom.users.resetPassword')}
confirmContent={translate('custom.users.resetPasswordTip')}
onConfirm={async () => {
await update(resource, {
id: record.id,

@ -10,5 +10,5 @@ export const broker = new TcBroker({
});
broker.start().then(() => {
console.log('已链接上Tailchat网络, TRANSPORTER: ', transporter);
console.log('Linked to Tailchat network, TRANSPORTER: ', transporter);
});

@ -10,9 +10,9 @@ import { apiRouter } from './router/api';
// 链接数据库
mongoose.connect(process.env.MONGO_URL!, (error: any) => {
if (!error) {
return console.info('数据库已连接成功');
return console.info('Datebase connected');
}
console.error('数据库连接失败', error);
console.error('Datebase connect error', error);
});
const BUILD_DIR = path.join(process.cwd(), 'build');

@ -101,6 +101,7 @@ After the compilation is complete, you can view the compiled image through `dock
<Tabs groupId="build">
<TabItem value="cli" label="One-command installation using cli" default>
```bash
npx tailchat-cli docker init
```
@ -109,10 +110,11 @@ Executing this command will ask you some configuration-related questions in an i
![](./assets/docker-init.png)
</TabItem>
</TabItem>
<TabItem value="public-image" label="Manually install from public image">
> A configuration file needs to be downloaded before starting to tell `docker-compose` how to start the image
> Download configuration files and configure environment variables from the repository:
> - [docker-compose.yml](https://raw.githubusercontent.com/msgbyte/tailchat/master/docker-compose.yml)
@ -131,9 +133,11 @@ Modify the configuration of the `docker-compose.env` file, the following fields
- `API_URL` is an externally accessible url address, used for file service access, it can be a domain name or an ip **If the sent picture cannot be displayed normally, this variable is not set**
- `SECRET` server-side encryption key, used to generate Token. The default is `tailchat`
</TabItem>
<TabItem value="source-code" label="Compile from source">
Need to modify the configuration before starting
Modify the configuration of the `docker-compose.env` file, the following fields are recommended to be modified:

Loading…
Cancel
Save