Add multi-platform publishing workflow

This commit is contained in:
王鹏
2026-09-14 17:40:52 +08:00
parent a466caa793
commit 0e24c9e235
41 changed files with 4980 additions and 58 deletions

View File

@@ -9,11 +9,12 @@ export function listCopyTemplate(query) {
})
}
// 查询所有启用的文案模板(源码明细页按钮用)
export function listEnabledTemplates() {
// 按场景查询所有启用的文案模板
export function listEnabledTemplates(templateScene) {
return request({
url: '/office/copyTemplate/enabled',
method: 'get'
method: 'get',
params: { templateScene }
})
}
@@ -49,4 +50,4 @@ export function delCopyTemplate(templateId) {
url: '/office/copyTemplate/' + templateId,
method: 'delete'
})
}
}

View File

@@ -0,0 +1,33 @@
import request from '@/utils/request'
export function listPublishProjects(query) {
return request({
url: '/office/publish/list',
method: 'get',
params: query
})
}
export function getPublishMaterial(codeId, platform) {
return request({
url: '/office/publish/material/' + codeId,
method: 'get',
params: { platform }
})
}
export function createPublishTask(data) {
return request({
url: '/office/publish/task',
method: 'post',
data: data
})
}
export function createPublishTasks(data) {
return request({
url: '/office/publish/tasks',
method: 'post',
data: data
})
}

View File

@@ -44,6 +44,7 @@
<script>
import { getToken } from "@/utils/auth";
import Sortable from "sortablejs";
export default {
props: {
@@ -67,6 +68,11 @@ export default {
isShowTip: {
type: Boolean,
default: true
},
// 是否允许拖动图片调整顺序
enableSort: {
type: Boolean,
default: false
}
},
data() {
@@ -81,9 +87,16 @@ export default {
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: []
fileList: [],
imageSortable: null
};
},
mounted() {
this.$nextTick(this.initImageSort);
},
beforeDestroy() {
if (this.imageSortable) this.imageSortable.destroy();
},
watch: {
value: {
handler(val) {
@@ -93,11 +106,7 @@ export default {
// 然后将数组转为对象数组
this.fileList = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(this.baseUrl) === -1) {
item = { name: this.baseUrl + item, url: item };
} else {
item = { name: item, url: item };
}
item = { name: item, url: this.getPreviewUrl(item) };
}
return item;
});
@@ -105,6 +114,7 @@ export default {
this.fileList = [];
return [];
}
this.$nextTick(this.initImageSort);
},
deep: true,
immediate: true
@@ -117,6 +127,21 @@ export default {
},
},
methods: {
initImageSort() {
if (!this.enableSort || this.imageSortable || !this.$refs.imageUpload) return;
const list = this.$refs.imageUpload.$el.querySelector('.el-upload-list');
if (!list) return;
this.imageSortable = Sortable.create(list, {
animation: 150,
draggable: '.el-upload-list__item',
onEnd: ({ oldIndex, newIndex }) => {
if (oldIndex === newIndex || oldIndex == null || newIndex == null) return;
const moved = this.fileList.splice(oldIndex, 1)[0];
this.fileList.splice(newIndex, 0, moved);
this.$emit("input", this.listToString(this.fileList));
}
});
},
// 上传前loading加载
handleBeforeUpload(file) {
let isImg = false;
@@ -155,12 +180,12 @@ export default {
// 上传成功回调
handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push({ name: res.fileName, url: res.fileName });
this.uploadList.push({ name: res.fileName, url: this.getPreviewUrl(res.fileName) });
this.uploadedSuccessfully();
} else {
this.number--;
this.$modal.closeLoading();
this.$modal.msgError(res.msg);
this.$modal.msgError((res && res.msg) || "上传图片失败,请检查服务器上传目录配置");
this.$refs.imageUpload.handleRemove(file);
this.uploadedSuccessfully();
}
@@ -175,6 +200,7 @@ export default {
},
// 上传失败
handleUploadError() {
if (this.number > 0) this.number--;
this.$modal.msgError("上传图片失败,请重试");
this.$modal.closeLoading();
},
@@ -193,13 +219,30 @@ export default {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
},
// 将后端返回的 /profile/... 相对路径转换为可访问的预览地址。
// 完整网络地址和 data/blob 本地预览地址保持不变。
getPreviewUrl(url) {
if (!url || typeof url !== "string") return "";
if (/^(https?:)?\/\//i.test(url) || /^(data|blob):/i.test(url)) return url;
if (url.indexOf(this.baseUrl + "/") === 0 || url === this.baseUrl) return url;
const path = url.indexOf("/") === 0 ? url : "/" + url;
return this.baseUrl + path;
},
// 提交表单时仍保存相对路径,避免把环境前缀或域名写入数据库。
getStorageUrl(url) {
if (!url || typeof url !== "string") return "";
if (url.indexOf(this.baseUrl + "/") === 0) {
return url.substring(this.baseUrl.length);
}
return url;
},
// 对象转成指定字符串分隔
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
if (list[i].url) {
strs += list[i].url.replace(this.baseUrl, "") + separator;
strs += this.getStorageUrl(list[i].url) + separator;
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
@@ -223,4 +266,3 @@ export default {
transform: translateY(0);
}
</style>

View File

@@ -615,7 +615,7 @@
});
},
loadCopyTemplates() {
listEnabledTemplates().then(response => {
listEnabledTemplates('CODE').then(response => {
this.copyTemplates = response.data || [];
this.selectDefaultArticleTemplate();
this.selectDefaultCopywritingTemplate();

View File

@@ -17,6 +17,12 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="模板场景" prop="templateScene">
<el-select v-model="queryParams.templateScene" placeholder="请选择模板场景" clearable>
<el-option label="源码文案" value="CODE" />
<el-option label="发货文案" value="DELIVERY" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="正常" value="0" />
@@ -46,6 +52,13 @@
<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="templateScene" width="110">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.templateScene === 'DELIVERY' ? 'warning' : ''">
{{ scope.row.templateScene === 'DELIVERY' ? '发货文案' : '源码文案' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="sortNum" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
@@ -75,6 +88,12 @@
<el-form-item label="模板分组" prop="templateGroup">
<el-input v-model="form.templateGroup" placeholder="请输入模板分组,例如:闲鱼、公众号" />
</el-form-item>
<el-form-item label="模板场景" prop="templateScene">
<el-radio-group v-model="form.templateScene">
<el-radio label="CODE">源码文案</el-radio>
<el-radio label="DELIVERY">发货文案</el-radio>
</el-radio-group>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="排序" prop="sortNum">
@@ -95,34 +114,37 @@
v-model="form.templateBody"
type="textarea"
:rows="20"
placeholder="请输入文案模板内容,支持以下占位符:
{codeName} 项目名称(完整)
{projectCode} 项目编号(如 A075)
{projectName} 纯项目名(去掉编号和基于...实现的前缀)
{codeDesc} 项目描述(纯文本)
{codeEnvironment} 运行环境
{frontendTechnology} 前端
{backendTechnology} 后端
{databaseTechnology} 数据库
{codeTechnology} 其他技术
{diskLink} 网盘地址
{screenshots} 截图列表(自动生成 markdown 格式)"
:placeholder="templateBodyPlaceholder"
/>
</el-form-item>
<el-alert type="info" :closable="false" style="margin-bottom:0">
<template slot="title">
<strong>支持的占位符:</strong>
<code>{codeName}</code> 项目名称 &nbsp;
<code>{projectCode}</code> 项目编号 &nbsp;
<code>{projectName}</code> 纯项目名 &nbsp;
<code>{codeDesc}</code> 描述(纯文本) &nbsp;
<code>{codeEnvironment}</code> 运行环境 &nbsp;
<code>{frontendTechnology}</code> 前端 &nbsp;
<code>{backendTechnology}</code> 后端 &nbsp;
<code>{databaseTechnology}</code> 数据库 &nbsp;
<code>{codeTechnology}</code> 其他技术 &nbsp;
<code>{diskLink}</code> 网盘地址 &nbsp;
<code>{screenshots}</code> 截图列表
<template v-if="form.templateScene === 'DELIVERY'">
<strong>发货文案占位符:</strong>
<code>{deliveryLinks}</code> 完整网盘文案 &nbsp;
<code>{quarkLink}</code> 夸克链接 &nbsp;
<code>{baiduLink}</code> 百度链接 &nbsp;
<code>{projectNum}</code> 项目编号 &nbsp;
<code>{projectName}</code> 项目名称 &nbsp;
<code>{sourceName}</code> 源码名称 &nbsp;
<code>{projectTechnology}</code> 项目技术 &nbsp;
<code>{projectDesc}</code> 项目描述 &nbsp;
<code>{videoLink}</code> 演示视频
</template>
<template v-else>
<strong>源码文案占位符:</strong>
<code>{codeName}</code> 项目名称 &nbsp;
<code>{projectCode}</code> 项目编号 &nbsp;
<code>{projectName}</code> 纯项目名 &nbsp;
<code>{codeDesc}</code> 描述(纯文本) &nbsp;
<code>{codeEnvironment}</code> 运行环境 &nbsp;
<code>{frontendTechnology}</code> 前端 &nbsp;
<code>{backendTechnology}</code> 后端 &nbsp;
<code>{databaseTechnology}</code> 数据库 &nbsp;
<code>{codeTechnology}</code> 其他技术 &nbsp;
<code>{diskLink}</code> 网盘地址 &nbsp;
<code>{screenshots}</code> 截图列表
</template>
</template>
</el-alert>
</el-form>
@@ -155,12 +177,14 @@ export default {
pageSize: 10,
templateName: null,
templateGroup: null,
templateScene: null,
status: null
},
form: {},
rules: {
templateName: [{ required: true, message: "按钮名称不能为空", trigger: "blur" }],
templateGroup: [{ required: true, message: "模板分组不能为空", trigger: "blur" }],
templateScene: [{ required: true, message: "模板场景不能为空", trigger: "change" }],
templateBody: [{ required: true, message: "模板内容不能为空", trigger: "blur" }],
sortNum: [{ required: true, message: "排序不能为空", trigger: "blur" }]
}
@@ -169,6 +193,14 @@ export default {
created() {
this.getList();
},
computed: {
templateBodyPlaceholder() {
if (this.form.templateScene === 'DELIVERY') {
return '请输入发货文案模板内容,例如:\n{deliveryLinks}\n\n项目部署文档\n文档地址和密码'
}
return '请输入源码文案模板内容,支持 {codeName}、{projectCode}、{projectName}、{codeDesc}、{diskLink}、{screenshots} 等占位符'
}
},
methods: {
getList() {
this.loading = true;
@@ -187,6 +219,7 @@ export default {
templateId: null,
templateName: null,
templateGroup: "默认分组",
templateScene: "CODE",
templateBody: null,
sortNum: 0,
status: "0"

View File

@@ -380,6 +380,7 @@ import {
checkProjectLinks,
checkSelectedProjectLinks
} from '@/api/office/project'
import { listEnabledTemplates } from '@/api/office/copyTemplate'
import { getToken } from '@/utils/auth'
export default {
@@ -404,6 +405,8 @@ export default {
total: 0,
// 项目清单表格数据
projectList: [],
// 启用的发货文案模板
deliveryTemplates: [],
// 弹出层标题
title: '',
// 是否显示弹出层
@@ -450,6 +453,7 @@ export default {
},
created() {
this.getList()
this.loadDeliveryTemplates()
},
methods: {
/** 查询项目清单列表 */
@@ -461,6 +465,12 @@ export default {
this.loading = false
})
},
/** 查询启用的发货文案模板 */
loadDeliveryTemplates() {
listEnabledTemplates('DELIVERY').then(response => {
this.deliveryTemplates = response.data || []
})
},
// 取消按钮
cancel() {
this.open = false
@@ -761,20 +771,42 @@ export default {
})
})
},
copyUrlToClipboard1(row) {
let content = ''
renderDeliveryTemplate(template, row) {
const linkSections = []
if (row.projectUrl) {
content += '夸克网盘\n'
content += '链接:' + row.projectUrl
}
if (content.length > 0) {
content += '\n\n'
linkSections.push('夸克网盘\n链接:' + row.projectUrl)
}
if (row.projectBaiduUrl) {
content += '百度网盘\n'
content += '链接:' + row.projectBaiduUrl
linkSections.push('百度网盘\n链接:' + row.projectBaiduUrl)
}
content += '\n\n项目部署文档\nhttps://www.yuque.com/feastcoding/cg5w9h/rbvp8w80h8tultgy 密码:el4q'
const variables = {
deliveryLinks: linkSections.join('\n\n'),
projectNum: row.projectNum || '',
projectName: row.projectName || '',
sourceName: row.projectName1 || '',
projectName1: row.projectName1 || '',
projectTechnology: row.projectNum1 || '',
projectDesc: row.projectDesc || '',
quarkLink: row.projectUrl || '',
projectUrl: row.projectUrl || '',
baiduLink: row.projectBaiduUrl || '',
projectBaiduUrl: row.projectBaiduUrl || '',
videoLink: row.projectVurl || '',
projectVurl: row.projectVurl || ''
}
let content = template.templateBody || ''
Object.keys(variables).forEach(key => {
content = content.split('{' + key + '}').join(variables[key])
})
return content
},
copyUrlToClipboard1(row, template) {
const selectedTemplate = template || this.deliveryTemplates[0]
if (!selectedTemplate) {
this.$message({ message: '请先在文案模板管理中配置并启用发货文案模板', type: 'warning' })
return
}
const content = this.renderDeliveryTemplate(selectedTemplate, row)
navigator.clipboard.writeText(content).then(() => {
this.$message({
message: '复制成功',

File diff suppressed because it is too large Load Diff