Add pin to top feature for environment variables

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-08 17:23:46 +00:00
parent 60cb86896f
commit e487bbd39f
4 changed files with 129 additions and 1 deletions

View File

@ -196,6 +196,40 @@ export default (app: Router) => {
}, },
); );
route.put(
'/pin',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.pin(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/unpin',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.unPin(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.post( route.post(
'/upload', '/upload',
upload.single('env'), upload.single('env'),

View File

@ -9,6 +9,7 @@ export class Env {
position?: number; position?: number;
name?: string; name?: string;
remarks?: string; remarks?: string;
isPinned?: 1 | 0;
constructor(options: Env) { constructor(options: Env) {
this.value = options.value; this.value = options.value;
@ -21,6 +22,7 @@ export class Env {
this.position = options.position; this.position = options.position;
this.name = options.name; this.name = options.name;
this.remarks = options.remarks || ''; this.remarks = options.remarks || '';
this.isPinned = options.isPinned || 0;
} }
} }
@ -42,4 +44,5 @@ export const EnvModel = sequelize.define<EnvInstance>('Env', {
position: DataTypes.NUMBER, position: DataTypes.NUMBER,
name: { type: DataTypes.STRING, unique: 'compositeIndex' }, name: { type: DataTypes.STRING, unique: 'compositeIndex' },
remarks: DataTypes.STRING, remarks: DataTypes.STRING,
isPinned: DataTypes.NUMBER,
}); });

View File

@ -147,6 +147,7 @@ export default class EnvService {
} }
try { try {
const result = await this.find(condition, [ const result = await this.find(condition, [
['isPinned', 'DESC'],
['position', 'DESC'], ['position', 'DESC'],
['createdAt', 'ASC'], ['createdAt', 'ASC'],
]); ]);
@ -190,6 +191,14 @@ export default class EnvService {
await this.set_envs(); await this.set_envs();
} }
public async pin(ids: number[]) {
await EnvModel.update({ isPinned: 1 }, { where: { id: ids } });
}
public async unPin(ids: number[]) {
await EnvModel.update({ isPinned: 0 }, { where: { id: ids } });
}
public async set_envs() { public async set_envs() {
const envs = await this.envs('', { const envs = await this.envs('', {
name: { [Op.not]: null }, name: { [Op.not]: null },

View File

@ -26,6 +26,8 @@ import {
CheckCircleOutlined, CheckCircleOutlined,
StopOutlined, StopOutlined,
UploadOutlined, UploadOutlined,
PushpinOutlined,
PushpinFilled,
} from '@ant-design/icons'; } from '@ant-design/icons';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@ -59,11 +61,15 @@ enum StatusColor {
enum OperationName { enum OperationName {
'启用', '启用',
'禁用', '禁用',
'置顶',
'取消置顶',
} }
enum OperationPath { enum OperationPath {
'enable', 'enable',
'disable', 'disable',
'pin',
'unpin',
} }
const type = 'DragableBodyRow'; const type = 'DragableBodyRow';
@ -181,7 +187,7 @@ const Env = () => {
{ {
title: intl.get('操作'), title: intl.get('操作'),
key: 'action', key: 'action',
width: 120, width: 160,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
@ -208,6 +214,23 @@ const Env = () => {
)} )}
</a> </a>
</Tooltip> </Tooltip>
<Tooltip
title={
isPc
? record.isPinned === 1
? intl.get('取消置顶')
: intl.get('置顶')
: ''
}
>
<a onClick={() => pinOrUnpinEnv(record, index)}>
{record.isPinned === 1 ? (
<PushpinFilled />
) : (
<PushpinOutlined />
)}
</a>
</Tooltip>
<Tooltip title={isPc ? intl.get('删除') : ''}> <Tooltip title={isPc ? intl.get('删除') : ''}>
<a onClick={() => deleteEnv(record, index)}> <a onClick={() => deleteEnv(record, index)}>
<DeleteOutlined /> <DeleteOutlined />
@ -305,6 +328,51 @@ const Env = () => {
setIsModalVisible(true); setIsModalVisible(true);
}; };
const pinOrUnpinEnv = (record: any, index: number) => {
Modal.confirm({
title: `确认${
record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
}`,
content: (
<>
{intl.get('确认')}
{record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')}
Env{' '}
<Paragraph
style={{ wordBreak: 'break-all', display: 'inline' }}
ellipsis={{ rows: 6, expandable: true }}
type="warning"
copyable
>
{record.name}: {record.value}
</Paragraph>{' '}
{intl.get('吗')}
</>
),
onOk() {
request
.put(
`${config.apiPrefix}envs/${
record.isPinned === 1 ? 'unpin' : 'pin'
}`,
[record.id],
)
.then(({ code, data }) => {
if (code === 200) {
message.success(
`${
record.isPinned === 1
? intl.get('取消置顶')
: intl.get('置顶')
}${intl.get('成功')}`,
);
getEnvs();
}
});
},
});
};
const deleteEnv = (record: any, index: number) => { const deleteEnv = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
title: intl.get('确认删除'), title: intl.get('确认删除'),
@ -589,6 +657,20 @@ const Env = () => {
> >
{intl.get('批量禁用')} {intl.get('批量禁用')}
</Button> </Button>
<Button
type="primary"
onClick={() => operateEnvs(2)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
{intl.get('批量置顶')}
</Button>
<Button
type="primary"
onClick={() => operateEnvs(3)}
style={{ marginLeft: 8, marginRight: 8 }}
>
{intl.get('批量取消置顶')}
</Button>
<span style={{ marginLeft: 8 }}> <span style={{ marginLeft: 8 }}>
{intl.get('已选择')} {intl.get('已选择')}
<a>{selectedRowIds?.length}</a> <a>{selectedRowIds?.length}</a>