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