feat: 优化文案模板分组与复制预览

This commit is contained in:
王鹏
2026-09-07 15:40:29 +08:00
parent 074bbda460
commit a466caa793
6 changed files with 474 additions and 15 deletions

View File

@@ -154,6 +154,14 @@
v-hasPermi="['office:code:query']"
>查看
</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-document"
@click="handleCopywriting(scope.row)"
v-hasPermi="['office:code:query']"
>文案
</el-button>
<el-button
size="mini"
type="text"
@@ -275,6 +283,90 @@
</div>
</el-dialog>
<!-- 文案预览对话框 -->
<el-dialog
:visible.sync="copywritingDialogVisible"
width="92%"
custom-class="copywriting-dialog-window"
append-to-body
@closed="resetCopywritingDialog"
>
<template slot="title">
<div class="copywriting-dialog-title">
<span>文案:</span>
<span
class="copywriting-project-name"
:title="copywritingProject.codeName || ''"
>{{ copywritingProject.codeName || '' }}</span>
</div>
</template>
<div v-loading="copywritingLoading" class="copywriting-shell">
<el-radio-group
v-if="groupedCopyTemplates.length"
v-model="copywritingGroupName"
size="small"
class="copywriting-group-tabs"
@change="handleCopywritingGroupChange"
>
<el-radio-button
v-for="group in groupedCopyTemplates"
:key="group.name"
:label="group.name"
>
{{ group.name }}
<span class="copywriting-group-count">{{ group.templates.length }}</span>
</el-radio-button>
</el-radio-group>
<div v-if="groupedCopyTemplates.length" class="copywriting-layout">
<div class="copywriting-template-panel">
<div class="copywriting-panel-title">模板列表</div>
<div v-if="activeCopywritingTemplates.length" class="copywriting-template-list">
<button
v-for="tpl in activeCopywritingTemplates"
:key="tpl.templateId"
type="button"
class="copywriting-template-item"
:class="{ 'is-active': tpl.templateId === copywritingTemplateId }"
@click="selectCopywritingTemplate(tpl)"
>
<span>{{ tpl.templateName }}</span>
<i
v-if="tpl.templateId === copywritingTemplateId"
class="el-icon-check"
/>
</button>
</div>
<div v-else class="copywriting-empty">暂无启用的文案模板</div>
</div>
<div class="copywriting-preview-panel">
<div class="copywriting-panel-title">文案预览</div>
<pre class="copywriting-preview">{{ copywritingContent }}</pre>
<div class="copywriting-preview-footer">
<span>字数:{{ copywritingContent.length }}</span>
<div>
<el-button
:disabled="!copywritingTemplateId"
@click="copyCurrentWriting(true)"
>复制并关闭</el-button>
<el-button
:type="copywritingCopied ? 'success' : 'primary'"
:icon="copywritingCopied ? 'el-icon-check' : 'el-icon-document-copy'"
:disabled="!copywritingTemplateId"
@click="copyCurrentWriting(false)"
>{{ copywritingCopied ? '已复制' : '复制文案' }}</el-button>
</div>
</div>
</div>
</div>
<div v-else class="copywriting-empty-state">
暂无启用的文案模板
</div>
</div>
</el-dialog>
<!-- 封面预览对话框 -->
<el-dialog :title="'封面预览'" :visible.sync="coverDialogVisible" width="500px" append-to-body>
<el-form label-width="80px">
@@ -340,6 +432,14 @@
// 是否显示弹出层
open: false,
openDetail: false,
// 文案预览弹窗
copywritingDialogVisible: false,
copywritingLoading: false,
copywritingProject: {},
copywritingTemplateId: null,
copywritingGroupName: '',
copywritingCopied: false,
copywritingCopyTimer: null,
// 查询参数
queryParams: {
pageNum: 1,
@@ -383,11 +483,36 @@
this.getList();
this.loadCopyTemplates();
},
beforeDestroy() {
if (this.copywritingCopyTimer) {
clearTimeout(this.copywritingCopyTimer);
}
},
computed: {
previewList() {
return this.form.fileList.map(i => {
return i.fileUrl
})
},
copywritingContent() {
const template = this.copyTemplates.find(item => item.templateId === this.copywritingTemplateId);
return template ? this.renderCopyTemplate(template, this.copywritingProject) : '';
},
groupedCopyTemplates() {
return this.copyTemplates.reduce((groups, template) => {
const groupName = (template.templateGroup || '').trim() || '默认分组';
let group = groups.find(item => item.name === groupName);
if (!group) {
group = { name: groupName, templates: [] };
groups.push(group);
}
group.templates.push(template);
return groups;
}, []);
},
activeCopywritingTemplates() {
const group = this.groupedCopyTemplates.find(item => item.name === this.copywritingGroupName);
return group ? group.templates : [];
}
},
methods: {
@@ -426,9 +551,10 @@
};
this.resetForm("form");
},
copyByTemplate(tpl) {
renderCopyTemplate(tpl, source) {
// 从数据库模板中提取并替换占位符
let codeName = this.form.codeName || '';
const code = source || {};
let codeName = code.codeName || '';
let projectCode = '';
let projectName = codeName;
@@ -448,16 +574,16 @@
// 项目描述转纯文本
let plainDesc = '';
if (this.form.codeDesc) {
let codeDesc = this.form.codeDesc.replace(/<\/p>/g, '</p>\n');
if (code.codeDesc) {
let codeDesc = code.codeDesc.replace(/<\/p>/g, '</p>\n');
plainDesc = html2Text(codeDesc.trimEnd());
}
// 生成截图列表
let screenshots = '';
let index = 0;
if (this.form.fileList && this.form.fileList.length > 0) {
for (let item of this.form.fileList) {
if (code.fileList && code.fileList.length > 0) {
for (let item of code.fileList) {
screenshots = screenshots + ++index + '.' + item.fileName + '\n![](' + item.fileUrl + ')\n\n';
}
} else {
@@ -470,13 +596,17 @@
content = content.replace(/{projectCode}/g, projectCode);
content = content.replace(/{projectName}/g, projectName);
content = content.replace(/{codeDesc}/g, plainDesc);
content = content.replace(/{codeEnvironment}/g, this.form.codeEnvironment || '');
content = content.replace(/{frontendTechnology}/g, this.form.frontendTechnology || '');
content = content.replace(/{backendTechnology}/g, this.form.backendTechnology || '');
content = content.replace(/{databaseTechnology}/g, this.form.databaseTechnology || '');
content = content.replace(/{codeTechnology}/g, this.form.codeTechnology || '');
content = content.replace(/{diskLink}/g, this.form.diskLink || '');
content = content.replace(/{codeEnvironment}/g, code.codeEnvironment || '');
content = content.replace(/{frontendTechnology}/g, code.frontendTechnology || '');
content = content.replace(/{backendTechnology}/g, code.backendTechnology || '');
content = content.replace(/{databaseTechnology}/g, code.databaseTechnology || '');
content = content.replace(/{codeTechnology}/g, code.codeTechnology || '');
content = content.replace(/{diskLink}/g, code.diskLink || '');
content = content.replace(/{screenshots}/g, screenshots);
return content;
},
copyByTemplate(tpl) {
const content = this.renderCopyTemplate(tpl, this.form);
navigator.clipboard.writeText(content).then(() => {
this.$message({ message: '复制成功', type: 'success' });
@@ -488,8 +618,49 @@
listEnabledTemplates().then(response => {
this.copyTemplates = response.data || [];
this.selectDefaultArticleTemplate();
this.selectDefaultCopywritingTemplate();
});
},
selectDefaultCopywritingTemplate() {
if (!this.copyTemplates.length) {
this.copywritingTemplateId = null;
this.copywritingGroupName = '';
return;
}
let selectedTemplate = this.copyTemplates.find(tpl => tpl.templateId === this.copywritingTemplateId);
if (!selectedTemplate) {
let lastTemplateId = '';
try {
lastTemplateId = window.localStorage.getItem('officeCopywritingTemplateId') || '';
} catch (error) {
lastTemplateId = '';
}
selectedTemplate = this.copyTemplates.find(tpl => String(tpl.templateId) === lastTemplateId)
|| this.copyTemplates[0];
}
this.selectCopywritingTemplate(selectedTemplate);
},
getCopywritingGroupName(template) {
return ((template && template.templateGroup) || '').trim() || '默认分组';
},
selectCopywritingTemplate(template) {
if (!template) {
return;
}
this.copywritingTemplateId = template.templateId;
this.copywritingGroupName = this.getCopywritingGroupName(template);
this.copywritingCopied = false;
try {
window.localStorage.setItem('officeCopywritingTemplateId', String(template.templateId));
} catch (error) {
}
},
handleCopywritingGroupChange(groupName) {
const group = this.groupedCopyTemplates.find(item => item.name === groupName);
if (group && group.templates.length) {
this.selectCopywritingTemplate(group.templates[0]);
}
},
selectDefaultArticleTemplate() {
if (!this.copyTemplates.length) {
this.articleTemplateId = null;
@@ -584,6 +755,57 @@
this.title = "查看源码信息";
});
},
/** 文案预览按钮操作 */
handleCopywriting(row) {
this.copywritingLoading = true;
this.copywritingProject = {};
this.copywritingTemplateId = null;
this.copywritingGroupName = '';
this.copywritingCopied = false;
this.copywritingDialogVisible = true;
this.selectDefaultCopywritingTemplate();
getCode(row.codeId).then(response => {
this.copywritingProject = response.data || {};
}).catch(() => {
this.copywritingDialogVisible = false;
}).finally(() => {
this.copywritingLoading = false;
});
},
resetCopywritingDialog() {
if (this.copywritingCopyTimer) {
clearTimeout(this.copywritingCopyTimer);
this.copywritingCopyTimer = null;
}
this.copywritingProject = {};
this.copywritingTemplateId = null;
this.copywritingGroupName = '';
this.copywritingCopied = false;
this.copywritingLoading = false;
},
copyCurrentWriting(closeAfterCopy) {
if (!this.copywritingTemplateId) {
this.$modal.msgError('请选择文案模板');
return;
}
navigator.clipboard.writeText(this.copywritingContent).then(() => {
if (closeAfterCopy) {
this.copywritingDialogVisible = false;
this.$modal.msgSuccess('文案复制成功');
return;
}
this.copywritingCopied = true;
if (this.copywritingCopyTimer) {
clearTimeout(this.copywritingCopyTimer);
}
this.copywritingCopyTimer = setTimeout(() => {
this.copywritingCopied = false;
this.copywritingCopyTimer = null;
}, 1800);
}).catch(error => {
this.$modal.msgError('复制失败:' + error);
});
},
showCoverDialog(row) {
this.currentRow = row
this.selectDefaultArticleTemplate()
@@ -702,3 +924,198 @@
}
};
</script>
<style scoped>
.copywriting-dialog-window {
max-width: 1180px;
}
.copywriting-dialog-title {
display: flex;
min-width: 0;
padding-right: 40px;
color: #303133;
font-size: 20px;
line-height: 24px;
}
.copywriting-project-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.copywriting-shell {
min-height: 420px;
}
.copywriting-group-tabs {
display: flex;
max-width: 100%;
margin-bottom: 16px;
padding-bottom: 4px;
overflow-x: auto;
white-space: nowrap;
}
.copywriting-group-count {
display: inline-block;
min-width: 18px;
margin-left: 4px;
padding: 0 5px;
border-radius: 9px;
background: rgba(144, 147, 153, 0.16);
font-size: 12px;
line-height: 18px;
}
.copywriting-layout {
display: flex;
height: 62vh;
min-height: 420px;
max-height: 620px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.copywriting-template-panel {
flex: 0 0 240px;
padding: 18px 14px;
border-right: 1px solid #dcdfe6;
background: #fafafa;
overflow-y: auto;
}
.copywriting-preview-panel {
display: flex;
flex: 1;
min-width: 0;
padding: 20px;
flex-direction: column;
}
.copywriting-panel-title {
margin-bottom: 14px;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.copywriting-template-list {
display: flex;
width: 100%;
flex-direction: column;
}
.copywriting-template-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 8px;
padding: 11px 12px;
border: 1px solid transparent;
border-radius: 4px;
outline: none;
background: transparent;
color: #606266;
font-size: 14px;
line-height: 20px;
text-align: left;
cursor: pointer;
transition: all 0.2s;
}
.copywriting-template-item:last-child {
margin-bottom: 0;
}
.copywriting-template-item:hover {
background: #ecf5ff;
color: #409eff;
}
.copywriting-template-item.is-active {
border-color: #b3d8ff;
background: #ecf5ff;
color: #409eff;
font-weight: 600;
}
.copywriting-template-item span {
min-width: 0;
overflow-wrap: anywhere;
}
.copywriting-template-item i {
flex: none;
margin-left: 8px;
font-weight: 700;
}
.copywriting-empty {
color: #909399;
font-size: 14px;
}
.copywriting-empty-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 420px;
border: 1px solid #ebeef5;
color: #909399;
}
.copywriting-preview {
flex: 1;
min-height: 0;
margin: 0;
padding: 16px;
overflow: auto;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fafafa;
color: #303133;
font-family: Consolas, "Courier New", monospace;
font-size: 14px;
line-height: 1.7;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.copywriting-preview-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 16px;
color: #606266;
}
@media (max-width: 768px) {
.copywriting-layout {
height: 68vh;
min-height: 0;
flex-direction: column;
}
.copywriting-template-panel {
flex: 0 0 auto;
max-height: 180px;
border-right: 0;
border-bottom: 1px solid #dcdfe6;
}
.copywriting-preview-panel {
padding: 14px;
}
.copywriting-preview-footer {
align-items: flex-start;
gap: 12px;
flex-direction: column;
}
}
</style>

View File

@@ -9,6 +9,14 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="模板分组" prop="templateGroup">
<el-input
v-model="queryParams.templateGroup"
placeholder="请输入模板分组"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="正常" value="0" />
@@ -37,6 +45,7 @@
<el-table v-loading="loading" :data="templateList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="按钮名称" align="center" prop="templateName" width="160" />
<el-table-column label="模板分组" align="center" prop="templateGroup" width="140" />
<el-table-column label="排序" align="center" prop="sortNum" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
@@ -63,6 +72,9 @@
<el-form-item label="按钮名称" prop="templateName">
<el-input v-model="form.templateName" placeholder="请输入按钮显示名称,例如:复制网站文案" />
</el-form-item>
<el-form-item label="模板分组" prop="templateGroup">
<el-input v-model="form.templateGroup" placeholder="请输入模板分组,例如:闲鱼、公众号" />
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="排序" prop="sortNum">
@@ -142,11 +154,13 @@ export default {
pageNum: 1,
pageSize: 10,
templateName: null,
templateGroup: null,
status: null
},
form: {},
rules: {
templateName: [{ required: true, message: "按钮名称不能为空", trigger: "blur" }],
templateGroup: [{ required: true, message: "模板分组不能为空", trigger: "blur" }],
templateBody: [{ required: true, message: "模板内容不能为空", trigger: "blur" }],
sortNum: [{ required: true, message: "排序不能为空", trigger: "blur" }]
}
@@ -172,6 +186,7 @@ export default {
this.form = {
templateId: null,
templateName: null,
templateGroup: "默认分组",
templateBody: null,
sortNum: 0,
status: "0"