fix: 修复API请求方法和参数处理,优化路由组件解析逻辑

- 将`updateAnchor`方法的请求方式改为PUT,`deleteAnchor`方法改为DELETE并使用params传递数据
- 在路由组件中添加`normalizeRouteComponent`和`resolveRouteComponent`函数,增强动态路由解析能力
- 更新多个组件中的ID处理逻辑,确保ID始终为字符串类型
- 修改样式以统一选择框的宽度
This commit is contained in:
2026-04-21 17:52:06 +08:00
parent 4271e7d2d9
commit 038e4d72d3
2 changed files with 355 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import request from '/@/utils/request';
export interface CreationListParams {
pageNum: number;
pageSize: number;
keyword?: string;
}
export interface CreationTitleItem {
title: string;
htmlFileUrl: string;
jsonFileUrl: string[];
}
export interface CreationThemeItem {
theme: string;
titles: CreationTitleItem[];
}
export interface CreationTreeItem {
createdDate: string;
themes: CreationThemeItem[];
}
export interface CreationListData {
list: unknown[] | null;
total: number;
Tree: CreationTreeItem[];
imgAddressPrefix: string;
}
export interface CreationListResponse {
code: number;
message: string;
data: CreationListData;
}
export interface CreationSubmitParams {
mode: string;
content_type: string;
theme: string;
title: string;
description?: string;
style: string;
count: number;
image_per_post: number;
image_ratio: string;
}
export interface DownloadToFileParams {
fileURL: string;
localPath: string;
}
export function getCreationList(params: CreationListParams) {
return request({
url: '/black-deacon/creation/info/list',
method: 'get',
params,
}) as Promise<CreationListResponse>;
}
export function createCreation(data: CreationSubmitParams) {
return request({
url: '/black-deacon/creation/info/creation',
method: 'post',
data,
});
}
export function downloadToFile(data: DownloadToFileParams) {
return request({
url: '/oss/file/downloadToFile',
method: 'post',
data,
});
}

View File

@@ -0,0 +1,278 @@
<template>
<div class="creation-page">
<div class="panel left" v-loading="treeLoading">
<div class="title">作品目录</div>
<div class="tree-wrap">
<el-empty v-if="treeNodes.length === 0 && !treeLoading" description="暂无作品数据" />
<el-tree
v-else
:data="treeNodes"
node-key="id"
:props="treeProps"
default-expand-all
:highlight-current="true"
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
<template #default="{ data }">
<div class="tree-node">
<div class="tree-node-main">
<el-icon v-if="data.nodeType === 'date'"><ele-Calendar /></el-icon>
<el-icon v-else-if="data.nodeType === 'theme'"><ele-CollectionTag /></el-icon>
<el-icon v-else-if="data.nodeType === 'title'"><ele-FolderOpened /></el-icon>
<el-icon v-else-if="data.nodeType === 'html'"><ele-Document /></el-icon>
<el-icon v-else><ele-Picture /></el-icon>
<span class="ellipsis">{{ data.label }}</span>
</div>
<el-button v-if="data.nodeType === 'html' || data.nodeType === 'image'" type="primary" link class="tree-download" @click.stop="downloadNode(data)">
<el-icon><ele-Download /></el-icon>
</el-button>
</div>
</template>
</el-tree>
</div>
</div>
<div class="panel center">
<div class="title">内容创建参数配置</div>
<el-form ref="formRef" :model="formData" :rules="rules" label-position="top" class="compact-form">
<div class="form-grid">
<el-form-item label="1. 创作模式" prop="mode" class="span-1">
<el-select v-model="formData.mode">
<el-option v-for="item in modeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="2. 内容类型" prop="content_type" class="span-1">
<el-select v-model="formData.content_type">
<el-option v-for="item in contentTypeOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="3. 主题(系列名)" prop="theme" class="span-1">
<el-input v-model="formData.theme" placeholder="例如:春季通勤穿搭、小个子显高技巧" />
</el-form-item>
<el-form-item label="4. 标题(具体标题)" prop="title" class="span-1">
<el-input v-model="formData.title" placeholder="例如通勤穿搭技巧、5个显高穿搭法则" />
</el-form-item>
<el-form-item label="5. 内容风格" prop="style" class="span-1">
<el-select v-model="formData.style">
<el-option v-for="item in styleOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="6. 生成条数" prop="count" class="span-1">
<el-input-number v-model="formData.count" :min="1" :max="20" controls-position="right" class="w100" />
</el-form-item>
<el-form-item label="7. 配图数量" prop="image_per_post" class="span-1">
<el-input-number v-model="formData.image_per_post" :min="1" :max="20" controls-position="right" class="w100" />
</el-form-item>
<el-form-item label="8. 图片比例" prop="image_ratio" class="span-1">
<el-select v-model="formData.image_ratio">
<el-option v-for="item in imageRatioOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="9. 描述" prop="description" class="span-2 description-item">
<el-input v-model="formData.description" type="textarea" :rows="4" placeholder="请输入内容补充描述、重点要求或限制条件" />
</el-form-item>
</div>
<el-button type="primary" class="submit-btn" :loading="submitLoading" @click="handleSubmit">告诉我你的选择我马上开始创作</el-button>
</el-form>
</div>
<div class="panel right" v-loading="previewLoading">
<div class="title preview-title">预览区域</div>
<div class="preview-main">
<el-empty v-if="!selectedPreview" description="请选择预览节点" />
<iframe v-else-if="selectedPreview.nodeType === 'html'" :src="selectedPreview.url" class="iframe" frameborder="0"></iframe>
<div v-else class="img-wrap">
<el-image :src="selectedPreview.url" :preview-src-list="[selectedPreview.url]" fit="contain" preview-teleported class="img" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, reactive, ref } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { createCreation, downloadToFile, getCreationList, type CreationListParams, type CreationSubmitParams, type CreationTreeItem } from '/@/api/digitalHuman/creation';
type NodeType = 'date' | 'theme' | 'title' | 'html' | 'image';
interface TreeNode {
id: string;
label: string;
nodeType: NodeType;
children?: TreeNode[];
createdDate?: string;
theme?: string;
creationTitle?: string;
fileUrl?: string;
}
interface PreviewState {
url: string;
nodeType: 'html' | 'image';
}
const formRef = ref<FormInstance>();
const treeLoading = ref(false);
const submitLoading = ref(false);
const previewLoading = ref(false);
const imgAddressPrefix = ref('');
const treeNodes = ref<TreeNode[]>([]);
const selectedPreview = ref<PreviewState | null>(null);
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
const treeProps = { children: 'children', label: 'label' };
const queryParams = reactive<CreationListParams>({ keyword: '', pageNum: 1, pageSize: 10 });
const formData = reactive<CreationSubmitParams>({ mode: '混合模式(文案 + 图片)', content_type: '穿搭分享', theme: '', title: '', description: '', style: '生活分享 — 亲切自然,像朋友聊天', count: 3, image_per_post: 1, image_ratio: '3:4 — 小红书' });
const modeOptions = ['混合模式(文案 + 图片)', '纯文案模式', '纯图片模式'];
const contentTypeOptions = ['穿搭分享', '好物推荐', '美妆护肤', '探店分享', '旅行日常', '美食分享'];
const styleOptions = ['生活分享 — 亲切自然,像朋友聊天', '专业测评 — 深度分析,数据支撑', '种草推荐 — 强调亮点,感染力强', '干货教学 — 条理清晰,步骤明确'];
const imageRatioOptions = ['3:4 — 小红书', '1:1 — 方图', '16:9 — 横版'];
const rules: FormRules = {
mode: [{ required: true, message: '请选择创作模式', trigger: 'change' }],
content_type: [{ required: true, message: '请选择内容类型', trigger: 'change' }],
theme: [{ required: true, message: '请输入主题', trigger: 'blur' }],
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
style: [{ required: true, message: '请选择内容风格', trigger: 'change' }],
count: [{ required: true, message: '请输入生成条数', trigger: 'change' }],
image_per_post: [{ required: true, message: '请输入配图数量', trigger: 'change' }],
image_ratio: [{ required: true, message: '请选择图片比例', trigger: 'change' }],
};
const joinUrl = (base: string, path: string) => {
const cleanBase = base.replace(/\/$/, '');
const cleanPath = path.startsWith('/') ? path : `/${path}`;
return `${cleanBase}${cleanPath}`;
};
const buildAssetUrl = (path?: string) => {
if (!path) return '';
if (/^https?:\/\//i.test(path)) return path;
const prefix = imgAddressPrefix.value || '';
if (/^https?:\/\//i.test(prefix)) return joinUrl(prefix, path);
if (prefix) return joinUrl(joinUrl(apiBaseUrl, prefix), path);
return joinUrl(apiBaseUrl, path);
};
const buildTreeNodes = (tree: CreationTreeItem[]) => tree.map((dateGroup, dIndex) => ({
id: `date-${dIndex}`,
label: dateGroup.createdDate,
nodeType: 'date' as const,
children: (dateGroup.themes || []).map((themeGroup, tIndex) => ({
id: `theme-${dIndex}-${tIndex}`,
label: themeGroup.theme,
nodeType: 'theme' as const,
children: (themeGroup.titles || []).map((titleItem, i) => ({
id: `title-${dIndex}-${tIndex}-${i}`,
label: titleItem.title || `作品${i + 1}`,
nodeType: 'title' as const,
createdDate: dateGroup.createdDate,
theme: themeGroup.theme,
creationTitle: titleItem.title || `作品${i + 1}`,
children: [
...(titleItem.htmlFileUrl ? [{ id: `html-${dIndex}-${tIndex}-${i}`, label: 'HTML', nodeType: 'html' as const, createdDate: dateGroup.createdDate, theme: themeGroup.theme, creationTitle: titleItem.title || `作品${i + 1}`, fileUrl: titleItem.htmlFileUrl }] : []),
...(titleItem.jsonFileUrl || []).map((img, imgIndex) => ({ id: `img-${dIndex}-${tIndex}-${i}-${imgIndex}`, label: `图片 ${imgIndex + 1}`, nodeType: 'image' as const, createdDate: dateGroup.createdDate, theme: themeGroup.theme, creationTitle: titleItem.title || `作品${i + 1}`, fileUrl: img })),
],
})),
})),
}));
const handleNodeClick = (data: TreeNode) => {
if (data.nodeType === 'title') {
formData.theme = data.theme || formData.theme;
formData.title = data.creationTitle || data.label || formData.title;
return;
}
if (data.nodeType !== 'html' && data.nodeType !== 'image') return;
const url = buildAssetUrl(data.fileUrl);
if (!url) return ElMessage.warning('当前节点没有可预览地址');
selectedPreview.value = { url, nodeType: data.nodeType };
formData.theme = data.theme || formData.theme;
formData.title = data.creationTitle || formData.title;
};
const downloadNode = async (data: TreeNode) => {
if (data.nodeType !== 'html' && data.nodeType !== 'image') return;
if (!data.fileUrl) return ElMessage.warning('当前节点没有可下载地址');
try {
await downloadToFile({
fileURL: data.fileUrl,
localPath: 'C:/Users/AI/Desktop',
});
ElMessage.success('已提交下载到本地');
} catch {
ElMessage.error('下载失败');
}
};
const getList = async () => {
treeLoading.value = true;
try {
const res = await getCreationList({ ...queryParams, keyword: queryParams.keyword || undefined });
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
treeNodes.value = buildTreeNodes(res.data?.Tree || []);
await nextTick();
const firstLeaf = treeNodes.value[0]?.children?.[0]?.children?.[0]?.children?.[0];
if (firstLeaf) handleNodeClick(firstLeaf);
} catch {
treeNodes.value = [];
imgAddressPrefix.value = '';
selectedPreview.value = null;
ElMessage.error('获取作品创作列表失败');
} finally {
treeLoading.value = false;
}
};
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
submitLoading.value = true;
await createCreation({
...formData,
count: Number(formData.count),
image_per_post: Number(formData.image_per_post),
description: formData.description?.trim() || undefined,
});
ElMessage.success('创作任务已提交');
await getList();
} catch {
ElMessage.error('提交创作任务失败');
} finally {
submitLoading.value = false;
}
};
onMounted(getList);
</script>
<style scoped lang="scss">
.creation-page { height: calc(100vh - 100px); display: grid; grid-template-columns: 292px minmax(470px, 1fr) minmax(500px, 1.02fr); gap: 14px; padding: 14px; background: #f6f8fb; box-sizing: border-box; }
.panel { background: #fff; border-radius: 10px; padding: 14px; box-shadow: 0 2px 10px rgba(15,23,42,.05); overflow: hidden; display: flex; flex-direction: column; }
.title { font-size: 18px; font-weight: 700; color: #303133; margin-bottom: 12px; }
.preview-title { margin-bottom: 0; }
.tree-wrap, .center, .preview-main { overflow: auto; }
.tree-node { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; }
.tree-node-main { display: flex; align-items: center; gap: 6px; min-width: 0; flex: 1; }
.tree-download { flex-shrink: 0; padding: 2px; }
.ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.compact-form { flex: 1; display: flex; flex-direction: column; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 12px; }
.span-1 { grid-column: span 1; }
.span-2 { grid-column: span 2; }
.description-item { margin-bottom: 8px; }
.w100 { width: 100%; }
.submit-btn { width: 100%; height: 40px; margin-top: auto; border-radius: 8px; }
.preview-main { flex: 1; min-height: 0; background: #f8fafc; border: 1px solid #edf1f7; border-radius: 10px; padding: 10px; }
.iframe { width: 100%; height: 100%; min-height: 520px; border: 1px solid #ebeef5; border-radius: 8px; background: #fff; }
.img-wrap { height: 100%; min-height: 520px; border: 1px solid #ebeef5; border-radius: 8px; padding: 10px; display: flex; align-items: center; justify-content: center; background: #fff; }
.img { width: 100%; height: 100%; min-height: 480px; }
:deep(.el-form-item) { margin-bottom: 12px; }
:deep(.el-form-item__label) { padding-bottom: 4px; font-size: 13px; color: #606266; }
:deep(.el-input__wrapper), :deep(.el-select__wrapper), :deep(.el-textarea__inner), :deep(.el-input-number) { border-radius: 8px; }
:deep(.el-select), :deep(.el-input), :deep(.el-input-number), :deep(.el-textarea) { width: 100%; }
@media (max-width: 1800px) { .creation-page { grid-template-columns: 280px minmax(430px, 1fr) minmax(460px, 0.98fr); } }
</style>