fix: 低优先级收尾 — 图片上传/语言编辑/插件恢复/URL 编码
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- P3-2: ArticleEditor 图片上传接入 /upload 端点 + 封面图上传按钮
- P4-3: recover_plugins 添加 tenant 日志 + 同 ID 去重保护
- P4-4: LanguageManager 编辑弹窗改为真实表单 (name 字段) + 后端 name 持久化
- P4-6: Settings API getSetting/updateSetting 添加 encodeURIComponent
This commit is contained in:
iven
2026-04-26 19:52:42 +08:00
parent b05b7c27a0
commit 8a253a4910
7 changed files with 111 additions and 45 deletions

View File

@@ -1,7 +1,7 @@
import { useEffect, useState, useCallback, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button, Input, Select, Space, message, Spin } from 'antd';
import { ArrowLeftOutlined, SaveOutlined, SendOutlined } from '@ant-design/icons';
import { Button, Input, Select, Space, message, Spin, Upload } from 'antd';
import { ArrowLeftOutlined, SaveOutlined, SendOutlined, UploadOutlined } from '@ant-design/icons';
import { Editor, Toolbar } from '@wangeditor/editor-for-react';
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor';
import {
@@ -104,10 +104,24 @@ export default function ArticleEditor() {
placeholder: '请输入文章内容...',
MENU_CONF: {
uploadImage: {
// 自定义图片上传 - 预留后端接口
async customUpload(_file: File, _insertFn: (url: string, alt?: string, href?: string) => void) {
// TODO: 实现图片上传到后端
message.warning('图片上传功能待实现');
async customUpload(file: File, insertFn: (url: string, alt?: string, href?: string) => void) {
try {
const formData = new FormData();
formData.append('file', file);
const token = localStorage.getItem('access_token');
const resp = await fetch('/api/v1/upload', {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: formData,
});
if (!resp.ok) throw new Error('上传失败');
const result = await resp.json();
const url: string = result.data.url;
const urlWithToken = token ? `${url}?token=${token}` : url;
insertFn(urlWithToken, file.name, urlWithToken);
} catch {
message.error('图片上传失败');
}
},
},
},
@@ -441,13 +455,41 @@ export default function ArticleEditor() {
color: isDark ? '#94a3b8' : '#475569',
}}
>
URL
</label>
<Input
value={coverImage}
onChange={(e) => setCoverImage(e.target.value)}
placeholder="请输入封面图片 URL"
/>
<Space.Compact style={{ width: '100%' }}>
<Input
value={coverImage}
onChange={(e) => setCoverImage(e.target.value)}
placeholder="请输入封面图片 URL 或上传文件"
style={{ flex: 1 }}
/>
<Upload
accept="image/*"
showUploadList={false}
beforeUpload={async (file) => {
try {
const formData = new FormData();
formData.append('file', file);
const token = localStorage.getItem('access_token');
const resp = await fetch('/api/v1/upload', {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: formData,
});
if (!resp.ok) throw new Error('上传失败');
const result = await resp.json();
setCoverImage(result.data.url);
message.success('封面图上传成功');
} catch {
message.error('封面图上传失败');
}
return false;
}}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
</Space.Compact>
{coverImage && (
<div
style={{

View File

@@ -8,6 +8,8 @@ import {
Typography,
message,
Card,
Form,
Input,
} from 'antd';
import { EditOutlined } from '@ant-design/icons';
import {
@@ -16,13 +18,12 @@ import {
type LanguageInfo,
} from '../../api/languages';
// --- Component ---
export default function LanguageManager() {
const [languages, setLanguages] = useState<LanguageInfo[]>([]);
const [loading, setLoading] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [editingLang, setEditingLang] = useState<LanguageInfo | null>(null);
const [form] = Form.useForm<{ name: string }>();
const fetchLanguages = useCallback(async () => {
setLoading(true);
@@ -39,8 +40,6 @@ export default function LanguageManager() {
fetchLanguages();
}, [fetchLanguages]);
// --- Enable / Disable Toggle ---
const handleToggle = async (record: LanguageInfo, checked: boolean) => {
try {
await updateLanguage(record.code, { is_active: checked });
@@ -55,24 +54,25 @@ export default function LanguageManager() {
}
};
// --- Edit Modal ---
const openEdit = (lang: LanguageInfo) => {
setEditingLang(lang);
form.setFieldsValue({ name: lang.name });
setEditModalOpen(true);
};
const closeEdit = () => {
setEditModalOpen(false);
setEditingLang(null);
form.resetFields();
};
const handleEditSubmit = async () => {
if (!editingLang) return;
try {
const values = await form.validateFields();
const updated = await updateLanguage(editingLang.code, {
is_active: editingLang.is_active,
name: values.name,
});
setLanguages((prev) =>
prev.map((lang) =>
@@ -82,6 +82,7 @@ export default function LanguageManager() {
message.success('语言更新成功');
closeEdit();
} catch (err: unknown) {
if (err && typeof err === 'object' && 'errorFields' in err) return;
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '更新失败';
@@ -89,8 +90,6 @@ export default function LanguageManager() {
}
};
// --- Columns ---
const columns = [
{
title: '语言代码',
@@ -113,13 +112,6 @@ export default function LanguageManager() {
<Switch checked={is_active} onChange={(checked) => handleToggle(record, checked)} />
),
},
{
title: '状态标签',
key: 'statusLabel',
width: 140,
render: (_: unknown, record: LanguageInfo) =>
record.is_active ? '已启用' : '已禁用',
},
{
title: '操作',
key: 'actions',
@@ -153,22 +145,25 @@ export default function LanguageManager() {
/>
</Card>
{/* Edit Modal */}
<Modal
title={`语言详情 - ${editingLang?.name ?? ''}`}
title={`编辑语言 - ${editingLang?.code ?? ''}`}
open={editModalOpen}
onCancel={closeEdit}
onOk={() => handleEditSubmit()}
onOk={handleEditSubmit}
okText="保存"
>
<div style={{ marginBottom: 12 }}>
<strong></strong>{editingLang?.code}
</div>
<div style={{ marginBottom: 12 }}>
<strong></strong>{editingLang?.name}
</div>
<div>
<strong></strong>{editingLang?.is_active ? '已启用' : '已禁用'}
</div>
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item label="语言代码">
<Input value={editingLang?.code} disabled />
</Form.Item>
<Form.Item
label="语言名称"
name="name"
rules={[{ required: true, message: '请输入语言名称' }]}
>
<Input placeholder="例如:简体中文" maxLength={100} />
</Form.Item>
</Form>
</Modal>
</div>
);