3 Commits

Author SHA1 Message Date
2ae927a851 功能:通过分页和任务控制功能增强任务管理
-为创建任务引入分页功能,以提
升用户体验。
-新增暂停、继续和删除任务的功能。
-更新了任务状态处理,包括'已暂停'状态及其相应的UI更新。
2026-04-29 09:52:56 +08:00
f137ae591e feat: update content creation task workflow 2026-04-27 17:01:53 +08:00
d516886fc9 feat: update content creation management 2026-04-27 10:23:27 +08:00
2 changed files with 534 additions and 92 deletions

59
package-lock.json generated
View File

@@ -923,6 +923,7 @@
"resolved": "https://registry.npmjs.org/@interactjs/core/-/core-1.10.27.tgz",
"integrity": "sha512-SliUr/3ZbLAdED8LokzYzWHWMdCB5Cq+UnpXuRy+BIod1j97m4IUFf/D1iIKUBBjBcucgXbz28z96WnenVCB7Q==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@interactjs/utils": "1.10.27"
}
@@ -993,6 +994,7 @@
"resolved": "https://registry.npmjs.org/@interactjs/modifiers/-/modifiers-1.10.27.tgz",
"integrity": "sha512-ei/qfoQ+9/8k6WzNzdNqHI6cWkIV576N4Ap16r5CoqOWwhA6Xzj3OMHf1g0t1O4eSq2HdJsVJn3eLNfw9HsbeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@interactjs/snappers": "1.10.27"
},
@@ -1059,7 +1061,8 @@
"version": "1.10.27",
"resolved": "https://registry.npmjs.org/@interactjs/utils/-/utils-1.10.27.tgz",
"integrity": "sha512-+qfLOio2OxQqg1cXSnRaCl+N8MQDQLDS9w+aOGxH8YLAhIMyt7Asxx/46//sT8orgsi16pmlBPtngPHT9s8zKw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@intlify/core-base": {
"version": "11.1.2",
@@ -1151,6 +1154,7 @@
"resolved": "https://registry.npmjs.org/@logicflow/core/-/core-2.2.1.tgz",
"integrity": "sha512-VzLPrCrT4eXnOLjoGQ5v4GUSay3+6rd3YNZD0qOJw4vME5e4WjQ5fd+hKK2zlIzgdRI4D54dXiEFJrS6xdV6yQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"classnames": "^2.3.2",
"lodash-es": "^4.17.21",
@@ -1904,6 +1908,7 @@
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/lodash": "*"
}
@@ -1914,6 +1919,7 @@
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -1985,6 +1991,7 @@
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0",
@@ -2160,6 +2167,7 @@
"resolved": "https://registry.npmjs.org/@uppy/core/-/core-2.3.4.tgz",
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@transloadit/prettier-bytes": "0.0.7",
"@uppy/store-default": "^2.1.1",
@@ -2191,6 +2199,7 @@
"resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@uppy/companion-client": "^2.2.2",
"@uppy/utils": "^4.1.2",
@@ -2419,6 +2428,7 @@
"resolved": "https://registry.npmjs.org/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
"license": "MIT",
"peer": true,
"dependencies": {
"is-url": "^1.2.4"
},
@@ -2451,6 +2461,7 @@
"resolved": "https://registry.npmjs.org/@wangeditor/core/-/core-1.1.19.tgz",
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/event-emitter": "^0.3.3",
"event-emitter": "^0.3.5",
@@ -2569,6 +2580,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2824,6 +2836,7 @@
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
@@ -3041,6 +3054,7 @@
"resolved": "https://registry.npmjs.org/dom7/-/dom7-3.0.0.tgz",
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"ssr-window": "^3.0.0-alpha.1"
}
@@ -3077,6 +3091,7 @@
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
@@ -3305,6 +3320,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -4097,7 +4113,8 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz",
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/is-number": {
"version": "7.0.0",
@@ -4284,13 +4301,15 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash-unified": {
"version": "1.0.3",
@@ -4307,32 +4326,37 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -4345,13 +4369,15 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/magic-string": {
"version": "0.30.21",
@@ -4464,6 +4490,7 @@
"resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.7.tgz",
"integrity": "sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mobx"
@@ -4528,6 +4555,7 @@
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -4814,6 +4842,7 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -5074,6 +5103,7 @@
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -5292,6 +5322,7 @@
"resolved": "https://registry.npmjs.org/slate/-/slate-0.72.8.tgz",
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"immer": "^9.0.6",
"is-plain-object": "^5.0.0",
@@ -5315,6 +5346,7 @@
"resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.6.3.tgz",
"integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.17.0"
}
@@ -5480,6 +5512,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -5557,6 +5590,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5626,6 +5660,7 @@
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -5794,6 +5829,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -5813,6 +5849,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",

View File

@@ -50,40 +50,88 @@
</div>
</div>
<div class="panel center">
<div class="title">内容创建参数配置</div>
<div class="form-header">
<div class="title">内容创建参数配置</div>
<el-badge :value="taskBadgeCount" :hidden="taskBadgeCount === 0" class="task-badge">
<el-button circle class="task-trigger" @click="taskDialogVisible = true">
<el-icon><ele-Bell /></el-icon>
</el-button>
</el-badge>
</div>
<el-form ref="formRef" :model="formData" :rules="rules" label-position="top" class="compact-form">
<div class="form-grid">
<el-form-item label="1. 创作模式" prop="mode" class="span-1"
><el-select v-model="formData.mode"><el-option v-for="item in modeOptions" :key="item" :label="item" :value="item" /></el-select
<el-form-item prop="mode" class="span-1"
><el-select v-model="formData.mode" placeholder="创作模式"
><el-option v-for="item in modeOptions" :key="item" :label="item" :value="item" /></el-select
></el-form-item>
<el-form-item label="2. 内容类型" prop="content_type" class="span-1"
><el-select v-model="formData.content_type"
<el-form-item prop="content_type" class="span-1"
><el-select v-model="formData.content_type" placeholder="内容类型" filterable allow-create default-first-option
><el-option v-for="item in contentTypeOptions" :key="item" :label="item" :value="item" /></el-select
></el-form-item>
<el-form-item label="3. 主题(系列名)" prop="theme" class="span-1"
><el-input v-model="formData.theme" placeholder="例如:春季通勤穿搭、小个子显高技巧"
<el-form-item prop="theme" class="span-1"
><el-input v-model="formData.theme" placeholder="主题(系列名),例如:春季通勤穿搭"
/></el-form-item>
<el-form-item label="4. 标题(具体标题)" prop="title" class="span-1"
><el-input v-model="formData.title" placeholder="例如:通勤穿搭技巧、5个显高穿搭法则"
<el-form-item prop="title" class="span-1"
><el-input v-model="formData.title" placeholder="标题(具体标题),例如:通勤穿搭技巧"
/></el-form-item>
<el-form-item label="5. 内容风格" prop="style" class="span-1"
><el-select v-model="formData.style"><el-option v-for="item in styleOptions" :key="item" :label="item" :value="item" /></el-select
<el-form-item prop="style" class="span-1"
><el-select v-model="formData.style" placeholder="内容风格" filterable allow-create default-first-option
><el-option v-for="item in styleOptions" :key="item" :label="item" :value="item" /></el-select
></el-form-item>
<el-form-item label="6. 生成条数" prop="count" class="span-1"
><el-input-number v-model="formData.count" :min="1" :max="3" controls-position="right" class="w100"
/></el-form-item>
<el-form-item v-if="showImageConfig" label="7. 每条配图数量" prop="image_per_post" class="span-1"
><el-input-number v-model="formData.image_per_post" :min="1" :max="3" controls-position="right" class="w100"
/></el-form-item>
<el-form-item v-if="showImageConfig" label="8. 图片比例" prop="image_ratio" class="span-1"
><el-select v-model="formData.image_ratio"
<el-form-item prop="count" class="span-1">
<div class="number-field">
<span class="number-label">生成条数</span>
<el-input-number v-model="formData.count" :min="1" :max="5" controls-position="right" class="w100" />
</div>
</el-form-item>
<el-form-item v-if="showImageConfig" prop="image_per_post" class="span-1">
<div class="number-field">
<span class="number-label">每条配图数量</span>
<el-input-number v-model="formData.image_per_post" :min="1" :max="3" controls-position="right" class="w100" />
</div>
</el-form-item>
<el-form-item v-if="showImageConfig" prop="image_ratio" class="span-1"
><el-select v-model="formData.image_ratio" placeholder="图片比例"
><el-option v-for="item in imageRatioOptions" :key="item" :label="item" :value="item" /></el-select
></el-form-item>
<el-form-item :label="showImageConfig ? '9. 描述' : '7. 描述'" prop="description" class="span-2 description-item"
><el-input v-model="formData.description" type="textarea" :rows="4" placeholder="请输入内容补充描述、重点要求或限制条件"
/></el-form-item>
<el-form-item prop="description" class="span-2 description-item">
<div class="chat-input-box">
<el-input
v-model="formData.description"
type="textarea"
:autosize="{ minRows: 3, maxRows: 8 }"
resize="none"
placeholder=" 说点什么"
class="chat-textarea"
/>
<div v-if="descriptionFiles.length" class="chat-file-list">
<el-tag v-for="file in descriptionFiles" :key="file.uid" closable type="info" effect="plain" @close="removeDescriptionFile(file.uid)">
{{ file.name }}
</el-tag>
</div>
<div class="chat-toolbar">
<div class="chat-actions">
<el-upload
v-model:file-list="descriptionFiles"
class="chat-upload"
multiple
:auto-upload="false"
:show-file-list="false"
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt"
>
<el-button text class="toolbar-btn">
<el-icon><ele-Paperclip /></el-icon>
<span>上传图片/文件</span>
</el-button>
</el-upload>
</div>
<el-button circle type="primary" class="chat-send-btn" :loading="submitLoading" @click="handleSubmit">
<el-icon><ele-Top /></el-icon>
</el-button>
</div>
</div>
</el-form-item>
</div>
<el-button type="primary" class="submit-btn" :loading="submitLoading" @click="handleSubmit">告诉我你的选择我马上开始创作</el-button>
</el-form>
</div>
<div class="panel right" v-loading="previewLoading">
@@ -96,12 +144,44 @@
</div>
</div>
</div>
<el-dialog v-model="taskDialogVisible" title="创作任务" width="680px" append-to-body class="task-dialog">
<div class="task-list">
<el-empty v-if="creationTasks.length === 0" description="暂无创作任务" />
<div v-for="task in pagedCreationTasks" :key="task.id" class="task-item">
<div class="task-item-header">
<div class="task-name">{{ task.title }}</div>
<el-tag :type="getTaskTagType(task.status)" effect="light">{{ getTaskStatusText(task.status) }}</el-tag>
</div>
<div class="task-summary">{{ task.summary }}</div>
<div class="task-time">
创建{{ task.createdAt }}<span v-if="task.updatedAt"> 更新{{ task.updatedAt }}</span>
</div>
<div v-if="task.error" class="task-error">{{ task.error }}</div>
<div class="task-actions-row">
<el-button v-if="task.status === 'running'" type="warning" link @click="pauseTask(task)">暂停</el-button>
<el-button v-if="task.status === 'paused'" type="primary" link :loading="submitLoading" @click="continueTask(task)">继续</el-button>
<el-button v-if="task.status === 'failed'" type="primary" link :loading="submitLoading" @click="retryTask(task)">重新执行</el-button>
<el-button type="danger" link @click="deleteTask(task.id)">删除</el-button>
</div>
</div>
</div>
<div v-if="creationTasks.length > taskPageSize" class="task-pagination" :style="{ marginTop: '20px' }">
<el-pagination
background
layout="prev, pager, next"
:current-page="taskPage"
:page-size="taskPageSize"
:total="creationTasks.length"
@current-change="handleTaskPageChange"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { ElMessage, type FormInstance, type FormRules, type UploadUserFile } from 'element-plus';
import {
createCreation,
downloadToFile,
@@ -110,6 +190,7 @@ import {
type CreationSubmitParams,
type CreationTreeItem,
} from '/@/api/digitalHuman/creation';
import { uploadFile } from '/@/api/knowledge/document';
type NodeType = 'date' | 'contentType' | 'theme' | 'title' | 'html' | 'image';
interface TreeNode {
@@ -128,12 +209,79 @@ interface PreviewState {
nodeType: 'html' | 'image';
}
const formRef = ref<FormInstance>();
type CreationTaskStatus = 'running' | 'success' | 'failed' | 'paused';
interface CreationTask {
id: number;
title: string;
summary: string;
status: CreationTaskStatus;
createdAt: string;
updatedAt?: string;
error?: string;
params: CreationSubmitParams;
}
const treeLoading = ref(false);
const submitLoading = ref(false);
const previewLoading = ref(false);
const imgAddressPrefix = ref('');
const treeNodes = ref<TreeNode[]>([]);
const selectedPreview = ref<PreviewState | null>(null);
const descriptionFiles = ref<UploadUserFile[]>([]);
const taskDialogVisible = ref(false);
const taskPage = ref(1);
const taskPageSize = ref(3);
const mockCreationParams: CreationSubmitParams = {
mode: '混合模式(文案 + 图片)',
content_type: '穿搭分享',
theme: '春季通勤穿搭',
title: '通勤穿搭技巧',
description: '模拟任务描述',
style: '生活分享 — 亲切自然,像朋友聊天',
count: 1,
image_per_post: 1,
image_ratio: '3:4 — 小红书',
};
const creationTasks = ref<CreationTask[]>([
{
id: 3,
title: '小个子显高穿搭法则',
summary: '穿搭分享 / 小个子显高技巧 / 5个显高穿搭法则',
status: 'running',
createdAt: '2026-04-27 10:20:18',
updatedAt: '2026-04-27 10:21:03',
params: { ...mockCreationParams, theme: '小个子显高技巧', title: '5个显高穿搭法则' },
},
{
id: 2,
title: '春季通勤穿搭技巧',
summary: '穿搭分享 / 春季通勤穿搭 / 通勤穿搭技巧',
status: 'success',
createdAt: '2026-04-27 09:45:12',
updatedAt: '2026-04-27 09:48:36',
params: { ...mockCreationParams },
},
{
id: 1,
title: '周末约会氛围感穿搭',
summary: '穿搭分享 / 约会穿搭 / 周末约会氛围感穿搭',
status: 'failed',
createdAt: '2026-04-27 09:12:05',
updatedAt: '2026-04-27 09:13:21',
error: '素材上传失败,请重新执行任务',
params: { ...mockCreationParams, theme: '约会穿搭', title: '周末约会氛围感穿搭' },
},
{
id: 2,
title: '氛围感穿搭',
summary: '穿搭分享 / 约会穿搭 / 氛围感穿搭',
status: 'failed',
createdAt: '2026-04-27 09:12:05',
updatedAt: '2026-04-27 09:13:21',
error: '素材上传失败,请重新执行任务',
params: { ...mockCreationParams, theme: '约会穿搭', title: '周末约会氛围感穿搭' },
},
]);
const taskIdSeed = ref(3);
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
const treeProps = { children: 'children', label: 'label' };
const queryParams = reactive<CreationListParams>({ keyword: '', pageNum: 1, pageSize: 10 });
@@ -158,6 +306,30 @@ const styleOptions = [
'干货教学 — 条理清晰,步骤明确',
];
const imageRatioOptions = ['3:4 — 小红书', '1:1 — 方图', '16:9 — 横版'];
const taskBadgeCount = computed(() => creationTasks.value.filter((task) => task.status !== 'success').length);
const pagedCreationTasks = computed(() => {
const start = (taskPage.value - 1) * taskPageSize.value;
return creationTasks.value.slice(start, start + taskPageSize.value);
});
watch(
() => creationTasks.value.length,
(total) => {
const maxPage = Math.max(1, Math.ceil(total / taskPageSize.value));
if (taskPage.value > maxPage) taskPage.value = maxPage;
}
);
const handleTaskPageChange = (page: number) => {
taskPage.value = page;
};
const formatTaskTime = () => {
const date = new Date();
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
};
const getTaskStatusText = (status: CreationTaskStatus) => ({ running: '执行中', success: '已完成', failed: '失败', paused: '已暂停' })[status];
const getTaskTagType = (status: CreationTaskStatus) =>
({ running: 'warning', success: 'success', failed: 'danger', paused: 'info' })[status] as 'warning' | 'success' | 'danger' | 'info';
const buildTaskSummary = (params: CreationSubmitParams) =>
`${params.content_type} / ${params.theme || '未填写主题'} / ${params.title || '未填写标题'}`;
watch(
() => formData.mode,
() => {
@@ -175,28 +347,32 @@ const rules: FormRules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
style: [{ required: true, message: '请选择内容风格', trigger: 'change' }],
count: [{ required: true, message: '请输入生成条数', trigger: 'change' }],
image_per_post: [{
required: true,
message: '请输入配图数量',
trigger: 'change',
validator: (rule, value, callback) => {
void rule;
if (!showImageConfig.value) return callback();
if (!value) return callback(new Error('请输入配图数量'));
callback();
image_per_post: [
{
required: true,
message: '请输入配图数量',
trigger: 'change',
validator: (rule, value, callback) => {
void rule;
if (!showImageConfig.value) return callback();
if (!value) return callback(new Error('请输入配图数量'));
callback();
},
},
}],
image_ratio: [{
required: true,
message: '请选择图片比例',
trigger: 'change',
validator: (rule, value, callback) => {
void rule;
if (!showImageConfig.value) return callback();
if (!value) return callback(new Error('请选择图片比例'));
callback();
],
image_ratio: [
{
required: true,
message: '请选择图片比例',
trigger: 'change',
validator: (rule, value, callback) => {
void rule;
if (!showImageConfig.value) return callback();
if (!value) return callback(new Error('请选择图片比例'));
callback();
},
},
}],
],
};
const joinUrl = (base: string, path: string) => `${base.replace(/\/$/, '')}${path.startsWith('/') ? path : `/${path}`}`;
const buildAssetUrl = (path?: string) => {
@@ -297,16 +473,6 @@ const downloadNode = async (data: TreeNode) => {
ElMessage.error('下载失败');
}
};
const findFirstPreviewNode = (nodes: TreeNode[]): TreeNode | null => {
for (const node of nodes) {
if (node.nodeType === 'html' || node.nodeType === 'image') return node;
if (node.children?.length) {
const matched = findFirstPreviewNode(node.children);
if (matched) return matched;
}
}
return null;
};
const getList = async () => {
treeLoading.value = true;
try {
@@ -314,9 +480,6 @@ const getList = async () => {
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
treeNodes.value = buildTreeNodes(res.data?.Tree || []);
selectedPreview.value = null;
await nextTick();
const firstLeaf = findFirstPreviewNode(treeNodes.value);
if (firstLeaf) handleNodeClick(firstLeaf);
} catch {
treeNodes.value = [];
imgAddressPrefix.value = '';
@@ -326,24 +489,91 @@ const getList = async () => {
treeLoading.value = false;
}
};
const removeDescriptionFile = (uid?: number) => {
descriptionFiles.value = descriptionFiles.value.filter((file) => file.uid !== uid);
};
const extractUploadUrl = (res: unknown) => {
const data = (res as { data?: Record<string, string> })?.data;
const root = res as Record<string, string>;
return data?.url || data?.fileUrl || data?.filePath || data?.path || root?.url || root?.fileUrl || root?.filePath || root?.path || '';
};
const buildDescription = async () => {
const description = formData.description?.trim() || '';
const rawFiles = descriptionFiles.value.map((item) => item.raw).filter(Boolean) as File[];
if (rawFiles.length === 0) return description || undefined;
const uploadedFiles = await Promise.all(
rawFiles.map(async (file) => {
const res = await uploadFile(file);
return {
name: file.name,
url: extractUploadUrl(res),
};
})
);
const attachmentText = uploadedFiles.map((file, index) => `${index + 1}. ${file.name}${file.url ? `${file.url}` : ''}`).join('\n');
return [description, `参考附件:\n${attachmentText}`].filter(Boolean).join('\n\n');
};
const runCreationTask = async (task: CreationTask) => {
try {
submitLoading.value = true;
selectedPreview.value = null;
task.status = 'running';
task.error = undefined;
task.updatedAt = formatTaskTime();
await createCreation(task.params);
task.status = 'success';
task.updatedAt = formatTaskTime();
ElMessage.success('创作任务已提交');
await getList();
} catch (error) {
task.status = 'failed';
task.updatedAt = formatTaskTime();
task.error = error instanceof Error ? error.message : '任务执行失败,请稍后重试';
ElMessage.error('提交创作任务失败');
} finally {
submitLoading.value = false;
}
};
const retryTask = async (task: CreationTask) => {
if (submitLoading.value) return;
await runCreationTask(task);
};
const deleteTask = (taskId: number) => {
creationTasks.value = creationTasks.value.filter((task) => task.id !== taskId);
};
const pauseTask = (task: CreationTask) => {
if (task.status !== 'running') return;
task.status = 'paused';
task.updatedAt = formatTaskTime();
};
const continueTask = async (task: CreationTask) => {
if (task.status !== 'paused' || submitLoading.value) return;
await runCreationTask(task);
};
const handleSubmit = async () => {
if (!formRef.value || submitLoading.value) return;
try {
await formRef.value.validate();
submitLoading.value = true;
selectedPreview.value = null;
await createCreation({
const description = await buildDescription();
const params: CreationSubmitParams = {
...formData,
count: Number(formData.count),
image_per_post: Number(formData.image_per_post),
description: formData.description?.trim() || undefined,
});
ElMessage.success('创作任务已提交');
await getList();
description,
};
const task: CreationTask = {
id: ++taskIdSeed.value,
title: params.title || `创作任务 ${taskIdSeed.value}`,
summary: buildTaskSummary(params),
status: 'running',
createdAt: formatTaskTime(),
params,
};
creationTasks.value.unshift(task);
taskDialogVisible.value = true;
await runCreationTask(task);
} catch {
ElMessage.error('提交创作任务失败');
} finally {
submitLoading.value = false;
}
};
onMounted(getList);
@@ -462,9 +692,80 @@ onMounted(getList);
color: #303133;
margin-bottom: 12px;
}
.preview-title {
.form-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.form-header .title {
margin-bottom: 0;
}
.task-trigger {
width: 34px;
height: 34px;
border-color: #e6ebf3;
background: #f8fafc;
}
.task-trigger:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary-light-5);
background: var(--el-color-primary-light-9);
}
.task-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 520px;
overflow: auto;
padding-right: 4px;
}
.task-item {
padding: 14px;
border: 1px solid #edf1f7;
border-radius: 12px;
background: #fbfcfe;
}
.task-item-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
}
.task-name {
font-size: 15px;
font-weight: 700;
color: #1f2d3d;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-summary,
.task-time {
font-size: 12px;
line-height: 1.6;
color: #7b8794;
}
.task-error {
margin-top: 8px;
padding: 8px 10px;
border-radius: 8px;
background: #fff2f0;
color: #f56c6c;
font-size: 12px;
line-height: 1.6;
}
.task-actions-row {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.preview-title {
margin-bottom: 2px;
}
.tree-wrap,
.center,
.preview-main {
@@ -501,7 +802,7 @@ onMounted(getList);
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 12px;
gap: 24px 12px;
}
.span-1 {
grid-column: span 1;
@@ -510,17 +811,91 @@ onMounted(getList);
grid-column: span 2;
}
.description-item {
margin-bottom: 8px;
margin-top: 2px;
margin-bottom: 2px;
}
.number-field {
position: relative;
width: 100%;
}
.number-label {
position: absolute;
left: 12px;
top: 50%;
z-index: 2;
transform: translateY(-50%);
font-size: 13px;
color: #909399;
pointer-events: none;
}
.number-field :deep(.el-input-number .el-input__inner) {
padding-left: 92px;
text-align: left;
}
.chat-input-box {
width: 100%;
padding: 14px 14px 10px;
border-radius: 22px;
background: #fff;
border: 1px solid #e9edf3;
box-shadow: 0 10px 24px rgba(31, 45, 61, 0.08);
box-sizing: border-box;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.chat-input-box:focus-within {
border-color: var(--el-color-primary-light-5);
box-shadow: 0 12px 28px rgba(64, 158, 255, 0.14);
}
.chat-textarea {
font-size: 14px;
}
.chat-file-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px 4px 2px;
}
.chat-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-top: 8px;
}
.chat-actions {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.chat-upload {
display: inline-flex;
}
.toolbar-btn {
height: 30px;
padding: 0 10px;
border-radius: 15px;
color: #303133;
background: #f5f7fa;
}
.toolbar-btn:hover {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.toolbar-btn :deep(.el-icon) {
margin-right: 4px;
}
.chat-send-btn {
width: 34px;
height: 34px;
font-size: 16px;
flex-shrink: 0;
}
.w100 {
width: 100%;
}
.submit-btn {
width: 100%;
height: 40px;
margin-top: auto;
border-radius: 8px;
}
.preview-main {
flex: 1;
min-height: 0;
@@ -553,19 +928,49 @@ onMounted(getList);
height: 100%;
min-height: 480px;
}
:deep(.chat-input-box .el-textarea__inner) {
padding: 0 2px;
border: none;
box-shadow: none;
background: transparent;
line-height: 1.6;
}
:deep(.chat-input-box .el-textarea__inner::placeholder) {
color: #a8abb2;
}
:deep(.el-form-item) {
margin-bottom: 12px;
margin-bottom: 2px;
}
:deep(.el-form-item__error) {
padding-top: 4px;
line-height: 16px;
white-space: nowrap;
}
:deep(.el-form-item__label) {
padding-bottom: 4px;
font-size: 13px;
color: #606266;
display: none;
}
:deep(.el-input__wrapper),
:deep(.el-select__wrapper),
:deep(.el-textarea__inner),
:deep(.el-input-number) {
border-radius: 8px;
border-radius: 12px;
box-shadow: 0 0 0 1px #e8edf5 inset;
background: #fbfcfe;
}
:deep(.el-input__wrapper:hover),
:deep(.el-select__wrapper:hover),
:deep(.el-textarea__inner:hover),
:deep(.el-input-number:hover) {
box-shadow: 0 0 0 1px #d6e2f2 inset;
}
:deep(.el-input__wrapper.is-focus),
:deep(.el-select__wrapper.is-focused),
:deep(.el-textarea__inner:focus) {
box-shadow: 0 0 0 1px var(--el-color-primary-light-5) inset;
}
:deep(.el-input__wrapper),
:deep(.el-select__wrapper) {
min-height: 40px;
}
:deep(.el-select),
:deep(.el-input),