ad9755f474
This commit standardizes the card header design across multiple edit pages to create a consistent and modern UI experience. Changes made: - Add gradient background colors to card headers - Implement decorative circular elements for visual appeal - Update icon colors to white with semi-transparent backgrounds - Standardize text colors and opacity for better readability - Add consistent padding and border radius - Maintain unique color schemes for different functional sections Modified files: - EditChannel.js - EditRedemption.js - EditToken.js - EditUser.js - AddUser.js The new design features: - Blue gradient for basic information sections - Green gradient for quota/permission settings - Purple gradient for access restrictions - Orange gradient for binding/group information - Consistent layout structure across all edit pages This update improves visual hierarchy and maintains brand consistency while enhancing the overall user experience.
455 lines
16 KiB
JavaScript
455 lines
16 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { API, isMobile, showError, showSuccess } from '../../helpers';
|
|
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
|
import {
|
|
Button,
|
|
Input,
|
|
Modal,
|
|
Select,
|
|
SideSheet,
|
|
Space,
|
|
Spin,
|
|
Typography,
|
|
Card,
|
|
Tag,
|
|
} from '@douyinfe/semi-ui';
|
|
import {
|
|
IconUser,
|
|
IconSave,
|
|
IconClose,
|
|
IconKey,
|
|
IconCreditCard,
|
|
IconLink,
|
|
IconUserGroup,
|
|
IconPlus,
|
|
} from '@douyinfe/semi-icons';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
const { Text, Title } = Typography;
|
|
|
|
const EditUser = (props) => {
|
|
const userId = props.editingUser.id;
|
|
const [loading, setLoading] = useState(true);
|
|
const [addQuotaModalOpen, setIsModalOpen] = useState(false);
|
|
const [addQuotaLocal, setAddQuotaLocal] = useState('');
|
|
const [inputs, setInputs] = useState({
|
|
username: '',
|
|
display_name: '',
|
|
password: '',
|
|
github_id: '',
|
|
oidc_id: '',
|
|
wechat_id: '',
|
|
email: '',
|
|
quota: 0,
|
|
group: 'default',
|
|
});
|
|
const [groupOptions, setGroupOptions] = useState([]);
|
|
const {
|
|
username,
|
|
display_name,
|
|
password,
|
|
github_id,
|
|
oidc_id,
|
|
wechat_id,
|
|
telegram_id,
|
|
email,
|
|
quota,
|
|
group,
|
|
} = inputs;
|
|
const handleInputChange = (name, value) => {
|
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
};
|
|
const fetchGroups = async () => {
|
|
try {
|
|
let res = await API.get(`/api/group/`);
|
|
setGroupOptions(
|
|
res.data.data.map((group) => ({
|
|
label: group,
|
|
value: group,
|
|
})),
|
|
);
|
|
} catch (error) {
|
|
showError(error.message);
|
|
}
|
|
};
|
|
const navigate = useNavigate();
|
|
const handleCancel = () => {
|
|
props.handleClose();
|
|
};
|
|
const loadUser = async () => {
|
|
setLoading(true);
|
|
let res = undefined;
|
|
if (userId) {
|
|
res = await API.get(`/api/user/${userId}`);
|
|
} else {
|
|
res = await API.get(`/api/user/self`);
|
|
}
|
|
const { success, message, data } = res.data;
|
|
if (success) {
|
|
data.password = '';
|
|
setInputs(data);
|
|
} else {
|
|
showError(message);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadUser().then();
|
|
if (userId) {
|
|
fetchGroups().then();
|
|
}
|
|
}, [props.editingUser.id]);
|
|
|
|
const submit = async () => {
|
|
setLoading(true);
|
|
let res = undefined;
|
|
if (userId) {
|
|
let data = { ...inputs, id: parseInt(userId) };
|
|
if (typeof data.quota === 'string') {
|
|
data.quota = parseInt(data.quota);
|
|
}
|
|
res = await API.put(`/api/user/`, data);
|
|
} else {
|
|
res = await API.put(`/api/user/self`, inputs);
|
|
}
|
|
const { success, message } = res.data;
|
|
if (success) {
|
|
showSuccess('用户信息更新成功!');
|
|
props.refresh();
|
|
props.handleClose();
|
|
} else {
|
|
showError(message);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
const addLocalQuota = () => {
|
|
let newQuota = parseInt(quota) + parseInt(addQuotaLocal);
|
|
setInputs((inputs) => ({ ...inputs, quota: newQuota }));
|
|
};
|
|
|
|
const openAddQuotaModal = () => {
|
|
setAddQuotaLocal('0');
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
<SideSheet
|
|
placement={'right'}
|
|
title={
|
|
<Space>
|
|
<Tag color="blue" shape="circle">{t('编辑')}</Tag>
|
|
<Title heading={4} className="m-0">
|
|
{t('编辑用户')}
|
|
</Title>
|
|
</Space>
|
|
}
|
|
headerStyle={{
|
|
borderBottom: '1px solid var(--semi-color-border)',
|
|
padding: '24px'
|
|
}}
|
|
bodyStyle={{
|
|
backgroundColor: 'var(--semi-color-bg-0)',
|
|
padding: '0'
|
|
}}
|
|
visible={props.visible}
|
|
width={isMobile() ? '100%' : 600}
|
|
footer={
|
|
<div className="flex justify-end bg-white">
|
|
<Space>
|
|
<Button
|
|
theme="solid"
|
|
size="large"
|
|
className="!rounded-full"
|
|
onClick={submit}
|
|
icon={<IconSave />}
|
|
loading={loading}
|
|
>
|
|
{t('提交')}
|
|
</Button>
|
|
<Button
|
|
theme="light"
|
|
size="large"
|
|
className="!rounded-full"
|
|
type="primary"
|
|
onClick={handleCancel}
|
|
icon={<IconClose />}
|
|
>
|
|
{t('取消')}
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
}
|
|
closeIcon={null}
|
|
onCancel={() => handleCancel()}
|
|
>
|
|
<Spin spinning={loading}>
|
|
<div className="p-6">
|
|
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
|
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
|
background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
|
|
position: 'relative'
|
|
}}>
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
|
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
|
<IconUser size="large" style={{ color: '#ffffff' }} />
|
|
</div>
|
|
<div className="relative">
|
|
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('基本信息')}</Text>
|
|
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('用户的基本账户信息')}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Text strong className="block mb-2">{t('用户名')}</Text>
|
|
<Input
|
|
placeholder={t('请输入新的用户名')}
|
|
onChange={(value) => handleInputChange('username', value)}
|
|
value={username}
|
|
autoComplete="new-password"
|
|
size="large"
|
|
className="!rounded-lg"
|
|
showClear
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Text strong className="block mb-2">{t('密码')}</Text>
|
|
<Input
|
|
type="password"
|
|
placeholder={t('请输入新的密码,最短 8 位')}
|
|
onChange={(value) => handleInputChange('password', value)}
|
|
value={password}
|
|
autoComplete="new-password"
|
|
size="large"
|
|
className="!rounded-lg"
|
|
prefix={<IconKey />}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Text strong className="block mb-2">{t('显示名称')}</Text>
|
|
<Input
|
|
placeholder={t('请输入新的显示名称')}
|
|
onChange={(value) => handleInputChange('display_name', value)}
|
|
value={display_name}
|
|
autoComplete="new-password"
|
|
size="large"
|
|
className="!rounded-lg"
|
|
showClear
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{userId && (
|
|
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
|
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
|
background: 'linear-gradient(135deg, #065f46 0%, #059669 50%, #10b981 100%)',
|
|
position: 'relative'
|
|
}}>
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
|
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
|
<IconUserGroup size="large" style={{ color: '#ffffff' }} />
|
|
</div>
|
|
<div className="relative">
|
|
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('权限设置')}</Text>
|
|
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('用户分组和额度管理')}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Text strong className="block mb-2">{t('分组')}</Text>
|
|
<Select
|
|
placeholder={t('请选择分组')}
|
|
search
|
|
allowAdditions
|
|
additionLabel={t(
|
|
'请在系统设置页面编辑分组倍率以添加新的分组:',
|
|
)}
|
|
onChange={(value) => handleInputChange('group', value)}
|
|
value={inputs.group}
|
|
autoComplete="new-password"
|
|
optionList={groupOptions}
|
|
size="large"
|
|
className="w-full !rounded-lg"
|
|
prefix={<IconUserGroup />}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex justify-between mb-2">
|
|
<Text strong>{t('剩余额度')}</Text>
|
|
<Text type="tertiary">{renderQuotaWithPrompt(quota)}</Text>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
placeholder={t('请输入新的剩余额度')}
|
|
onChange={(value) => handleInputChange('quota', value)}
|
|
value={quota}
|
|
type="number"
|
|
autoComplete="new-password"
|
|
size="large"
|
|
className="flex-1 !rounded-lg"
|
|
prefix={<IconCreditCard />}
|
|
/>
|
|
<Button
|
|
onClick={openAddQuotaModal}
|
|
size="large"
|
|
className="!rounded-lg"
|
|
icon={<IconPlus />}
|
|
>
|
|
{t('添加额度')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
<Card className="!rounded-2xl shadow-sm border-0">
|
|
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
|
background: 'linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%)',
|
|
position: 'relative'
|
|
}}>
|
|
<div className="absolute inset-0 overflow-hidden">
|
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
|
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
|
</div>
|
|
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
|
<IconLink size="large" style={{ color: '#ffffff' }} />
|
|
</div>
|
|
<div className="relative">
|
|
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('绑定信息')}</Text>
|
|
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('第三方账户绑定状态(只读)')}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Text strong className="block mb-2">{t('已绑定的 GitHub 账户')}</Text>
|
|
<Input
|
|
value={github_id}
|
|
autoComplete="new-password"
|
|
placeholder={t(
|
|
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
)}
|
|
readonly
|
|
size="large"
|
|
className="!rounded-lg"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Text strong className="block mb-2">{t('已绑定的 OIDC 账户')}</Text>
|
|
<Input
|
|
value={oidc_id}
|
|
placeholder={t(
|
|
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
)}
|
|
readonly
|
|
size="large"
|
|
className="!rounded-lg"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Text strong className="block mb-2">{t('已绑定的微信账户')}</Text>
|
|
<Input
|
|
value={wechat_id}
|
|
autoComplete="new-password"
|
|
placeholder={t(
|
|
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
)}
|
|
readonly
|
|
size="large"
|
|
className="!rounded-lg"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Text strong className="block mb-2">{t('已绑定的邮箱账户')}</Text>
|
|
<Input
|
|
value={email}
|
|
autoComplete="new-password"
|
|
placeholder={t(
|
|
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
)}
|
|
readonly
|
|
size="large"
|
|
className="!rounded-lg"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Text strong className="block mb-2">{t('已绑定的 Telegram 账户')}</Text>
|
|
<Input
|
|
value={telegram_id}
|
|
autoComplete="new-password"
|
|
placeholder={t(
|
|
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
|
)}
|
|
readonly
|
|
size="large"
|
|
className="!rounded-lg"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</Spin>
|
|
</SideSheet>
|
|
|
|
<Modal
|
|
centered={true}
|
|
visible={addQuotaModalOpen}
|
|
onOk={() => {
|
|
addLocalQuota();
|
|
setIsModalOpen(false);
|
|
}}
|
|
onCancel={() => setIsModalOpen(false)}
|
|
closable={null}
|
|
title={
|
|
<div className="flex items-center">
|
|
<IconPlus className="mr-2" />
|
|
{t('添加额度')}
|
|
</div>
|
|
}
|
|
>
|
|
<div className="mb-4">
|
|
<Text type="secondary" className="block mb-2">
|
|
{`${t('新额度')}${renderQuota(quota)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(quota + parseInt(addQuotaLocal || 0))}`}
|
|
</Text>
|
|
</div>
|
|
<Input
|
|
placeholder={t('需要添加的额度(支持负数)')}
|
|
onChange={(value) => {
|
|
setAddQuotaLocal(value);
|
|
}}
|
|
value={addQuotaLocal}
|
|
type="number"
|
|
autoComplete="new-password"
|
|
size="large"
|
|
className="!rounded-lg"
|
|
prefix={<IconCreditCard />}
|
|
/>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default EditUser;
|