feat(ui): 账号列表恢复「标签」列(含就地编辑)

按反馈把标签列加回来。这列不只是展示 —— **有 `account:tag` 权限时就是一个
内联编辑框**:选已有标签或直接输入新标签即保存,无权限时退化为只读 Tag。

因此一并恢复它所依赖的东西:
- `handleSetTag`(单条改标签)
- `tagColorMap` + `TAG_COLORS`(只读态的标签配色)
- `useMemo` 导入

所以现在改标签有三个入口,各管一段场景:
| 入口 | 场景 |
| --- | --- |
| 列表「标签」列 | 单条快速改 |
| 列表「批量打标签」 | 一批一起改 |
| 详情页状态卡 | 在账号全貌下改(S6) |

列表列最终为:**账号 / 状态 / 绑定 / 积分·鱼翅 / 标签 /(凭据三列,开关控制)/ 操作**

验证:pytest 500 passed / 6 skipped;ruff 全绿;前端 build 通过、lint 0 errors。
This commit is contained in:
yml2213
2026-10-04 17:12:13 +08:00
parent e24af9ad75
commit a21e4a7e2a
+52
View File
@@ -12,6 +12,10 @@ import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
const TAG_COLORS = [
'blue', 'green', 'cyan', 'geekblue', 'purple', 'orange', 'magenta', 'volcano',
];
const { TextArea } = Input;
const { Text } = Typography;
@@ -194,6 +198,14 @@ export default function AccountsPage() {
}
};
const tagColorMap = useMemo(() => {
const map: Record<string, string> = {};
tags.forEach((tg, i) => {
map[tg] = TAG_COLORS[i % TAG_COLORS.length];
});
return map;
}, [tags]);
const handleBatchTag = async () => {
if (selectedCount === 0) {
message.warning('请先选择账号');
@@ -213,6 +225,17 @@ export default function AccountsPage() {
}
};
const handleSetTag = async (accountId: number, tag: string) => {
try {
await accountApi.setTag(accountId, tag);
message.success('标签已更新');
loadAccounts();
loadTags();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
const handleDelete = async (id: number) => {
try {
await accountApi.delete(id);
@@ -351,6 +374,35 @@ export default function AccountsPage() {
</Space>
),
},
{
title: '标签',
dataIndex: 'tag',
width: 170,
render: (tag: string, record: AccountItem) => {
// 有权限时直接就地改(Select 输入新标签即保存);无权限则只读展示。
if (canTag) {
return (
<Select
mode="tags"
maxCount={1}
allowClear
showSearch
optionFilterProp="label"
size="small"
placeholder="选择或输入标签"
style={{ width: 150 }}
value={tag ? [tag] : []}
onChange={(values) => {
const value = values.at(-1)?.trim() || '';
if (value !== tag) handleSetTag(record.id, value);
}}
options={tags.map((value) => ({ value, label: value }))}
/>
);
}
return tag ? <Tag color={tagColorMap[tag]}>{tag}</Tag> : <Text type="secondary">-</Text>;
},
},
];
if (canViewFull && sensitiveVisible) {