feat: TaskForm 应用场景表单弹窗化

- 应用场景表单从内联改为 el-dialog 弹窗
- 新增「添加应用场景」按钮,编辑也走弹窗
- 表单 label-position=top,布局更清晰
- 移除旧的 application-form 行内布局样式
- 新增 application-header 弹性布局
- 弹窗在保存/取消后自动关闭
This commit is contained in:
developer
2026-07-12 13:50:03 +08:00
parent 032317da7b
commit 2315c0d08f
+62 -51
View File
@@ -39,8 +39,9 @@ const applicationStatusOptions: { label: string; value: TaskApplication["status"
];
const applications = ref<TaskApplication[]>([]);
const appUrlTitles = ref<Record<number, string>>({});
const applicationLoading = ref(false);
const appUrlTitles = ref<Record<number, string>>({});
const applicationDialogVisible = ref(false);
const applicationSaving = ref(false);
const editingApplicationId = ref<number | null>(null);
const applicationForm = ref<{
@@ -55,6 +56,11 @@ const applicationForm = ref<{
status: "TODO",
});
const openApplicationDialog = () => {
resetApplicationForm();
applicationDialogVisible.value = true;
};
const resetApplicationForm = () => {
editingApplicationId.value = null;
applicationForm.value = {
@@ -70,6 +76,7 @@ const loadApplications = async (targetTaskNum: string) => {
try {
const res = await getTaskApplications(targetTaskNum);
applications.value = res?.data || [];
// 异步抓取标题
const map: Record<number, string> = {};
await Promise.all(
applications.value
@@ -111,6 +118,7 @@ const saveApplication = async () => {
await createTaskApplication(taskNum.value, payload);
ElMessage.success("应用场景已添加");
}
applicationDialogVisible.value = false;
resetApplicationForm();
await loadApplications(taskNum.value);
} finally {
@@ -126,6 +134,7 @@ const editApplication = (item: TaskApplication) => {
resourceUrl: item.resourceUrl || "",
status: item.status || "TODO",
};
applicationDialogVisible.value = true;
};
const removeApplication = async (item: TaskApplication) => {
@@ -145,6 +154,7 @@ const convertMaterialUrls = async () => {
const bareUrls = new Set<string>();
raw.replace(/https?:\/\/[^\s)\u3001\uFF09\u300D\u300B<>]+/g, (url) => {
const clean = url.replace(/[.。,;!??)】」』\]]+$/, "");
// 跳过 [text](url) 内的
if (!raw.includes(`](${clean})`)) bareUrls.add(clean);
return url;
});
@@ -274,12 +284,14 @@ const escapeAttr = (s: string) => s.replace(/"/g, "&quot;").replace(/&/g, "&amp;
function renderMarkdown(raw: string, urlTitles: Record<string, string> = {}): string {
if (!raw.trim()) return "";
let html = raw;
// 1) [text](url) → &lt;a&gt; 占位,防止后续裸 URL 匹配进属性里
const links: string[] = [];
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m: string, text: string, url: string) => {
const tag = `<a href="${escapeAttr(url)}" target="_blank" rel="noopener">${escapeHtml(text)}</a>`;
links.push(tag);
return `__LINK_${links.length - 1}__`;
});
// 2) 裸 URL → &lt;a&gt; 占位
html = html.replace(/https?:\/\/[^\s)\u3001\uFF09\u300D\u300B<>]+/g, (url) => {
const clean = url.replace(/[.。,;!??)】」』\]]+$/, "");
const label = urlTitles[clean] || url;
@@ -287,7 +299,9 @@ function renderMarkdown(raw: string, urlTitles: Record<string, string> = {}): st
links.push(tag);
return `__LINK_${links.length - 1}__`;
});
// 3) 换行
html = html.replace(/\n/g, "<br>");
// 4) 还原占位
for (let i = 0; i < links.length; i++) {
html = html.replace(`__LINK_${i}__`, links[i]);
}
@@ -349,7 +363,12 @@ async function previewMaterial() {
</template>
<el-input
v-if="!showPreview"
<el-input v-model="materialUrl" type="textarea" :rows="6" placeholder="支持 Markdown 格式&#10;例如:&#10;参考 [Vue.js 文档](https://vuejs.org/guide)&#10;源码在 https://github.com/vuejs/core" />
v-model="materialUrl"
type="textarea"
:rows="6"
placeholder="支持 Markdown 格式&#10;例如:&#10;参考 [Vue.js 文档](https://vuejs.org/guide)&#10;源码在 https://github.com/vuejs/core"
/>
<div v-else class="material-preview" v-html="materialPreview" />
</el-form-item>
</div>
@@ -389,7 +408,10 @@ async function previewMaterial() {
</div>
<div class="group" v-if="isUpdateMode" v-loading="applicationLoading">
<h3>应用场景</h3>
<div class="application-header">
<h3>应用场景</h3>
<el-button type="primary" @click="openApplicationDialog()">添加应用场景</el-button>
</div>
<div class="application-list" v-if="applications.length > 0">
<div class="application-item" v-for="item in applications" :key="item.id">
<div class="application-main">
@@ -409,33 +431,33 @@ async function previewMaterial() {
</div>
</div>
<p v-else class="empty-text">暂未设置应用场景</p>
<div class="application-form">
<el-input v-model="applicationForm.title" placeholder="应用项目" />
<el-input
v-model="applicationForm.description"
type="textarea"
:rows="3"
placeholder="应用描述"
/>
<el-input v-model="applicationForm.resourceUrl" placeholder="相关链接" />
<div class="application-form-row">
<el-select v-model="applicationForm.status" placeholder="状态">
<el-option
v-for="option in applicationStatusOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-button type="success" :loading="applicationSaving" @click="saveApplication">
{{ editingApplicationId ? "更新" : "添加" }}
</el-button>
<el-button v-if="editingApplicationId" @click="resetApplicationForm">取消</el-button>
</div>
</div>
</div>
<el-dialog v-model="applicationDialogVisible" :title="editingApplicationId ? '编辑应用场景' : '添加应用场景'" width="500px">
<el-form label-position="top">
<el-form-item label="应用项目" required>
<el-input v-model="applicationForm.title" placeholder="例如:用Vue3重构个人博客" />
</el-form-item>
<el-form-item label="应用描述">
<el-input v-model="applicationForm.description" type="textarea" :rows="4" placeholder="描述如何应用所学内容" />
</el-form-item>
<el-form-item label="相关链接">
<el-input v-model="applicationForm.resourceUrl" placeholder="https://..." />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="applicationForm.status" placeholder="状态">
<el-option v-for="option in applicationStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="applicationDialogVisible = false">取消</el-button>
<el-button type="success" :loading="applicationSaving" @click="saveApplication">
{{ editingApplicationId ? '更新' : '添加' }}
</el-button>
</template>
</el-dialog>
<div class="action-row">
<el-button type="success" size="large" @click="submitTask">{{ buttonName }}</el-button>
<el-button size="large" @click="cancelTask">取消</el-button>
@@ -502,7 +524,7 @@ async function previewMaterial() {
}
.material-preview :deep(a)::after {
content: " \u2197";
content: " ";
font-size: 11px;
opacity: 0.5;
}
@@ -517,6 +539,17 @@ async function previewMaterial() {
grid-template-columns: 1fr;
}
.application-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.application-header h3 {
margin: 0;
}
.application-list {
display: flex;
flex-direction: column;
@@ -558,20 +591,6 @@ async function previewMaterial() {
margin-top: 8px;
}
.application-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.application-form-row {
display: grid;
grid-template-columns: minmax(160px, 220px) auto auto;
gap: 10px;
align-items: center;
justify-content: start;
}
.empty-text {
margin: 0 0 12px;
color: var(--text-secondary);
@@ -603,13 +622,5 @@ async function previewMaterial() {
flex-direction: column;
}
.application-form-row {
grid-template-columns: 1fr;
}
.application-form-row .el-button {
width: 100%;
margin: 0;
}
}
</style>