✨ feat: add admin-only "remark" support for users
* backend
- model: add `Remark` field (varchar 255, `json:"remark,omitempty"`); AutoMigrate handles schema change automatically
- controller:
* accept `remark` on user create/update endpoints
* hide remark from regular users (`GetSelf`) by zero-ing the field before JSON marshalling
* clarify inline comment explaining the omitempty behaviour
* frontend (React / Semi UI)
- AddUser.js & EditUser.js: add “Remark” input for admins
- UsersTable.js:
* remove standalone “Remark” column
* show remark as a truncated Tag next to username with Tooltip for full text
* import Tooltip component
- i18n: reuse existing translations where applicable
This commit enables administrators to label users with private notes while ensuring those notes are never exposed to the users themselves.
This commit is contained in:
@@ -459,6 +459,9 @@ func GetSelf(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
// Hide admin remarks: set to empty to trigger omitempty tag, ensuring the remark field is not included in JSON returned to regular users
|
||||
user.Remark = ""
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
||||
@@ -41,6 +41,7 @@ type User struct {
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
LinuxDOId string `json:"linux_do_id" gorm:"column:linux_do_id;index"`
|
||||
Setting string `json:"setting" gorm:"type:text;column:setting"`
|
||||
Remark string `json:"remark,omitempty" gorm:"type:varchar(255)" validate:"max=255"`
|
||||
}
|
||||
|
||||
func (user *User) ToBaseUser() *UserBase {
|
||||
@@ -366,6 +367,7 @@ func (user *User) Edit(updatePassword bool) error {
|
||||
"display_name": newUser.DisplayName,
|
||||
"group": newUser.Group,
|
||||
"quota": newUser.Quota,
|
||||
"remark": newUser.Remark,
|
||||
}
|
||||
if updatePassword {
|
||||
updates["password"] = newUser.Password
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
@@ -110,6 +111,27 @@ const UsersTable = () => {
|
||||
{
|
||||
title: t('用户名'),
|
||||
dataIndex: 'username',
|
||||
render: (text, record) => {
|
||||
const remark = record.remark;
|
||||
if (!remark) {
|
||||
return <span>{text}</span>;
|
||||
}
|
||||
const maxLen = 10;
|
||||
const displayRemark = remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark;
|
||||
return (
|
||||
<Space spacing={2}>
|
||||
<span>{text}</span>
|
||||
<Tooltip content={remark} position="top" showArrow>
|
||||
<Tag color='white' size='large' shape='circle' className="!text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 rounded-full flex-shrink-0" style={{ backgroundColor: '#10b981' }} />
|
||||
{displayRemark}
|
||||
</div>
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('分组'),
|
||||
|
||||
@@ -1658,5 +1658,6 @@
|
||||
"清除失效兑换码": "Clear invalid redemption codes",
|
||||
"确定清除所有失效兑换码?": "Are you sure you want to clear all invalid redemption codes?",
|
||||
"将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "This will delete all used, disabled, and expired redemption codes, this operation cannot be undone.",
|
||||
"选择过期时间(可选,留空为永久)": "Select expiration time (optional, leave blank for permanent)"
|
||||
"选择过期时间(可选,留空为永久)": "Select expiration time (optional, leave blank for permanent)",
|
||||
"请输入备注(仅管理员可见)": "Please enter a remark (only visible to administrators)"
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
IconClose,
|
||||
IconKey,
|
||||
IconUserAdd,
|
||||
IconEdit,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -27,10 +28,11 @@ const AddUser = (props) => {
|
||||
username: '',
|
||||
display_name: '',
|
||||
password: '',
|
||||
remark: '',
|
||||
};
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { username, display_name, password } = inputs;
|
||||
const { username, display_name, password, remark } = inputs;
|
||||
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
@@ -175,6 +177,20 @@ const AddUser = (props) => {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong className="block mb-2">{t('备注')}</Text>
|
||||
<Input
|
||||
placeholder={t('请输入备注(仅管理员可见)')}
|
||||
onChange={(value) => handleInputChange('remark', value)}
|
||||
value={remark}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconEdit />}
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
IconLink,
|
||||
IconUserGroup,
|
||||
IconPlus,
|
||||
IconEdit,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -42,6 +43,7 @@ const EditUser = (props) => {
|
||||
email: '',
|
||||
quota: 0,
|
||||
group: 'default',
|
||||
remark: '',
|
||||
});
|
||||
const [groupOptions, setGroupOptions] = useState([]);
|
||||
const {
|
||||
@@ -55,6 +57,7 @@ const EditUser = (props) => {
|
||||
email,
|
||||
quota,
|
||||
group,
|
||||
remark,
|
||||
} = inputs;
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
@@ -247,6 +250,20 @@ const EditUser = (props) => {
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text strong className="block mb-2">{t('备注')}</Text>
|
||||
<Input
|
||||
placeholder={t('请输入备注(仅管理员可见)')}
|
||||
onChange={(value) => handleInputChange('remark', value)}
|
||||
value={remark}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconEdit />}
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user