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 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, """).replace(/&/g, "&
|
||||
function renderMarkdown(raw: string, urlTitles: Record<string, string> = {}): string {
|
||||
if (!raw.trim()) return "";
|
||||
let html = raw;
|
||||
// 1) [text](url) → <a> 占位,防止后续裸 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 → <a> 占位
|
||||
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 格式 例如: 参考 [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>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user