Commit 1f6ec017 by Apple\Apple

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.
parent 1c4819b2
...@@ -459,6 +459,9 @@ func GetSelf(c *gin.Context) { ...@@ -459,6 +459,9 @@ func GetSelf(c *gin.Context) {
}) })
return 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{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"message": "", "message": "",
......
...@@ -41,6 +41,7 @@ type User struct { ...@@ -41,6 +41,7 @@ type User struct {
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
LinuxDOId string `json:"linux_do_id" gorm:"column:linux_do_id;index"` LinuxDOId string `json:"linux_do_id" gorm:"column:linux_do_id;index"`
Setting string `json:"setting" gorm:"type:text;column:setting"` 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 { func (user *User) ToBaseUser() *UserBase {
...@@ -366,6 +367,7 @@ func (user *User) Edit(updatePassword bool) error { ...@@ -366,6 +367,7 @@ func (user *User) Edit(updatePassword bool) error {
"display_name": newUser.DisplayName, "display_name": newUser.DisplayName,
"group": newUser.Group, "group": newUser.Group,
"quota": newUser.Quota, "quota": newUser.Quota,
"remark": newUser.Remark,
} }
if updatePassword { if updatePassword {
updates["password"] = newUser.Password updates["password"] = newUser.Password
......
...@@ -26,6 +26,7 @@ import { ...@@ -26,6 +26,7 @@ import {
Space, Space,
Table, Table,
Tag, Tag,
Tooltip,
Typography Typography
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
...@@ -110,6 +111,27 @@ const UsersTable = () => { ...@@ -110,6 +111,27 @@ const UsersTable = () => {
{ {
title: t('用户名'), title: t('用户名'),
dataIndex: 'username', 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('分组'), title: t('分组'),
......
...@@ -1658,5 +1658,6 @@ ...@@ -1658,5 +1658,6 @@
"清除失效兑换码": "Clear invalid redemption codes", "清除失效兑换码": "Clear invalid redemption codes",
"确定清除所有失效兑换码?": "Are you sure you want to clear all 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.", "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "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)"
} }
\ No newline at end of file
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
IconClose, IconClose,
IconKey, IconKey,
IconUserAdd, IconUserAdd,
IconEdit,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
...@@ -27,10 +28,11 @@ const AddUser = (props) => { ...@@ -27,10 +28,11 @@ const AddUser = (props) => {
username: '', username: '',
display_name: '', display_name: '',
password: '', password: '',
remark: '',
}; };
const [inputs, setInputs] = useState(originInputs); const [inputs, setInputs] = useState(originInputs);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { username, display_name, password } = inputs; const { username, display_name, password, remark } = inputs;
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
...@@ -175,6 +177,20 @@ const AddUser = (props) => { ...@@ -175,6 +177,20 @@ const AddUser = (props) => {
required required
/> />
</div> </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> </div>
</Card> </Card>
</div> </div>
......
...@@ -22,6 +22,7 @@ import { ...@@ -22,6 +22,7 @@ import {
IconLink, IconLink,
IconUserGroup, IconUserGroup,
IconPlus, IconPlus,
IconEdit,
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
...@@ -42,6 +43,7 @@ const EditUser = (props) => { ...@@ -42,6 +43,7 @@ const EditUser = (props) => {
email: '', email: '',
quota: 0, quota: 0,
group: 'default', group: 'default',
remark: '',
}); });
const [groupOptions, setGroupOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]);
const { const {
...@@ -55,6 +57,7 @@ const EditUser = (props) => { ...@@ -55,6 +57,7 @@ const EditUser = (props) => {
email, email,
quota, quota,
group, group,
remark,
} = inputs; } = inputs;
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
...@@ -247,6 +250,20 @@ const EditUser = (props) => { ...@@ -247,6 +250,20 @@ const EditUser = (props) => {
showClear showClear
/> />
</div> </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> </div>
</Card> </Card>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment