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:
Apple\Apple
2025-06-16 03:20:54 +08:00
parent d2b47969da
commit 3ac02879de
6 changed files with 63 additions and 2 deletions

View File

@@ -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": "",

View File

@@ -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

View File

@@ -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('分组'),

View File

@@ -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)"
}

View File

@@ -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>

View File

@@ -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>