| New file |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="UTF-8" /> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | | <title>Prompt配置</title> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css" /> |
| | | <style> |
| | | [v-cloak] { display: none; } |
| | | body { |
| | | margin: 0; |
| | | font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; |
| | | background: |
| | | radial-gradient(1200px 520px at 8% -10%, rgba(24, 118, 210, 0.14), transparent 50%), |
| | | radial-gradient(900px 480px at 100% 0%, rgba(22, 160, 133, 0.11), transparent 56%), |
| | | linear-gradient(180deg, #f4f8fb 0%, #f8fbfd 100%); |
| | | color: #223244; |
| | | } |
| | | .page-shell { |
| | | max-width: 1700px; |
| | | margin: 14px auto; |
| | | padding: 0 14px 14px; |
| | | } |
| | | .hero { |
| | | border-radius: 18px; |
| | | color: #fff; |
| | | padding: 16px 18px; |
| | | background: linear-gradient(135deg, #0f4774 0%, #1f6fb2 45%, #249b8f 100%); |
| | | box-shadow: 0 14px 30px rgba(16, 65, 103, 0.22); |
| | | } |
| | | .hero-top { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | flex-wrap: wrap; |
| | | } |
| | | .hero-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | min-width: 0; |
| | | } |
| | | .hero-title .main { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | letter-spacing: 0.3px; |
| | | } |
| | | .hero-title .sub { |
| | | margin-top: 3px; |
| | | font-size: 12px; |
| | | opacity: 0.92; |
| | | } |
| | | .hero-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | justify-content: flex-end; |
| | | } |
| | | .summary-grid { |
| | | margin-top: 12px; |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | } |
| | | .summary-card { |
| | | border-radius: 12px; |
| | | padding: 10px 12px; |
| | | min-height: 62px; |
| | | border: 1px solid rgba(255, 255, 255, 0.22); |
| | | background: rgba(255, 255, 255, 0.14); |
| | | backdrop-filter: blur(4px); |
| | | } |
| | | .summary-card .k { |
| | | font-size: 11px; |
| | | opacity: 0.88; |
| | | } |
| | | .summary-card .v { |
| | | margin-top: 6px; |
| | | font-size: 24px; |
| | | font-weight: 700; |
| | | line-height: 1.1; |
| | | } |
| | | .workspace { |
| | | margin-top: 12px; |
| | | display: grid; |
| | | grid-template-columns: 280px 380px minmax(0, 1fr); |
| | | gap: 12px; |
| | | min-height: calc(100vh - 170px); |
| | | } |
| | | .panel { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 0; |
| | | border-radius: 18px; |
| | | border: 1px solid #dce6f1; |
| | | background: |
| | | radial-gradient(700px 180px at -10% 0%, rgba(45, 120, 200, 0.05), transparent 55%), |
| | | radial-gradient(640px 200px at 110% 10%, rgba(35, 155, 133, 0.06), transparent 60%), |
| | | rgba(255, 255, 255, 0.95); |
| | | box-shadow: 0 14px 30px rgba(28, 53, 84, 0.08); |
| | | overflow: hidden; |
| | | } |
| | | .panel-head { |
| | | padding: 14px 16px 10px; |
| | | border-bottom: 1px solid #e4ebf3; |
| | | background: rgba(249, 251, 255, 0.92); |
| | | } |
| | | .panel-head-title { |
| | | font-size: 15px; |
| | | font-weight: 700; |
| | | color: #24384d; |
| | | } |
| | | .panel-head-sub { |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: #7a8ea4; |
| | | line-height: 1.5; |
| | | } |
| | | .panel-body { |
| | | flex: 1 1 auto; |
| | | min-height: 0; |
| | | overflow: auto; |
| | | padding: 12px; |
| | | } |
| | | .scene-card { |
| | | border-radius: 14px; |
| | | border: 1px solid #e2eaf4; |
| | | padding: 12px; |
| | | background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%); |
| | | box-shadow: 0 10px 22px rgba(16, 43, 72, 0.06); |
| | | cursor: pointer; |
| | | transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; |
| | | } |
| | | .scene-card + .scene-card { |
| | | margin-top: 10px; |
| | | } |
| | | .scene-card:hover { |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 12px 24px rgba(16, 43, 72, 0.10); |
| | | } |
| | | .scene-card.active { |
| | | border-color: #84b5eb; |
| | | box-shadow: 0 14px 26px rgba(31, 111, 178, 0.15); |
| | | background: linear-gradient(180deg, #fdfefe 0%, #f4faff 100%); |
| | | } |
| | | .scene-head { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | .scene-code { |
| | | margin-top: 4px; |
| | | font-size: 11px; |
| | | color: #8da0b5; |
| | | word-break: break-all; |
| | | font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace; |
| | | } |
| | | .scene-stats { |
| | | margin-top: 10px; |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 8px; |
| | | } |
| | | .mini-stat { |
| | | border-radius: 10px; |
| | | border: 1px solid #e7eef7; |
| | | padding: 8px 10px; |
| | | background: #fff; |
| | | } |
| | | .mini-stat .k { |
| | | font-size: 11px; |
| | | color: #7f92a8; |
| | | } |
| | | .mini-stat .v { |
| | | margin-top: 4px; |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | color: #2a3e55; |
| | | } |
| | | .version-toolbar { |
| | | display: flex; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | margin-bottom: 10px; |
| | | } |
| | | .version-card { |
| | | border-radius: 14px; |
| | | border: 1px solid #e2eaf4; |
| | | background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%); |
| | | box-shadow: 0 10px 20px rgba(18, 44, 74, 0.06); |
| | | padding: 12px; |
| | | transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; |
| | | } |
| | | .version-card + .version-card { |
| | | margin-top: 10px; |
| | | } |
| | | .version-card.active { |
| | | border-color: #77aee9; |
| | | box-shadow: 0 14px 26px rgba(31, 111, 178, 0.15); |
| | | transform: translateY(-1px); |
| | | } |
| | | .version-card.published { |
| | | background: linear-gradient(180deg, #fffef8 0%, #fffaf0 100%); |
| | | border-color: #f0d79f; |
| | | } |
| | | .version-head { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 10px; |
| | | } |
| | | .version-title { |
| | | font-size: 14px; |
| | | font-weight: 700; |
| | | color: #25384c; |
| | | } |
| | | .version-meta { |
| | | margin-top: 6px; |
| | | font-size: 12px; |
| | | color: #74879d; |
| | | line-height: 1.7; |
| | | } |
| | | .version-actions { |
| | | margin-top: 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | .version-actions-left, |
| | | .version-actions-right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | .editor-toolbar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | margin-bottom: 12px; |
| | | } |
| | | .editor-toolbar-right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | .editor-form { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | } |
| | | .editor-full { |
| | | grid-column: 1 / -1; |
| | | } |
| | | .field-label { |
| | | margin-bottom: 4px; |
| | | font-size: 12px; |
| | | color: #6f859d; |
| | | } |
| | | .editor-textarea .el-textarea__inner { |
| | | min-height: 430px !important; |
| | | font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace; |
| | | font-size: 12px; |
| | | line-height: 1.7; |
| | | color: #243447; |
| | | background: #fbfdff; |
| | | border-color: #dbe7f3; |
| | | } |
| | | .editor-hint { |
| | | margin-top: 10px; |
| | | padding: 10px 12px; |
| | | border-radius: 12px; |
| | | border: 1px solid #e7edf6; |
| | | background: linear-gradient(180deg, #fcfdff 0%, #f7fbff 100%); |
| | | color: #5c7087; |
| | | font-size: 12px; |
| | | line-height: 1.7; |
| | | } |
| | | .editor-stats { |
| | | margin-top: 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | color: #7e91a6; |
| | | font-size: 12px; |
| | | } |
| | | .empty-shell { |
| | | min-height: 220px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-direction: column; |
| | | border: 1px dashed #d1dbe8; |
| | | border-radius: 14px; |
| | | background: rgba(255, 255, 255, 0.6); |
| | | color: #7d8ea4; |
| | | text-align: center; |
| | | padding: 18px; |
| | | } |
| | | .empty-shell strong { |
| | | font-size: 14px; |
| | | color: #374b63; |
| | | margin-bottom: 6px; |
| | | } |
| | | @media (max-width: 1380px) { |
| | | .workspace { |
| | | grid-template-columns: 260px minmax(320px, 1fr); |
| | | } |
| | | .editor-panel { |
| | | grid-column: 1 / -1; |
| | | } |
| | | } |
| | | @media (max-width: 960px) { |
| | | .summary-grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | .workspace { |
| | | grid-template-columns: 1fr; |
| | | min-height: auto; |
| | | } |
| | | .editor-form { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div id="app" class="page-shell" v-cloak> |
| | | <div class="hero"> |
| | | <div class="hero-top"> |
| | | <div class="hero-title"> |
| | | <div v-html="headerIcon" style="display:flex;"></div> |
| | | <div> |
| | | <div class="main">AI配置 - Prompt中心</div> |
| | | <div class="sub">按场景管理 Prompt 版本,支持草稿编辑、发布切换和快速复制优化</div> |
| | | </div> |
| | | </div> |
| | | <div class="hero-actions"> |
| | | <el-button size="mini" @click="goLlmConfig">LLM配置</el-button> |
| | | <el-button size="mini" @click="restoreDefaults">补齐默认Prompt</el-button> |
| | | <el-button size="mini" @click="reloadAll">刷新</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="summary-grid"> |
| | | <div class="summary-card"> |
| | | <div class="k">场景数</div> |
| | | <div class="v">{{ summary.sceneCount }}</div> |
| | | </div> |
| | | <div class="summary-card"> |
| | | <div class="k">版本总数</div> |
| | | <div class="v">{{ summary.totalTemplates }}</div> |
| | | </div> |
| | | <div class="summary-card"> |
| | | <div class="k">已发布</div> |
| | | <div class="v">{{ summary.publishedTemplates }}</div> |
| | | </div> |
| | | <div class="summary-card"> |
| | | <div class="k">草稿数</div> |
| | | <div class="v">{{ summary.draftTemplates }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="workspace"> |
| | | <div class="panel"> |
| | | <div class="panel-head"> |
| | | <div class="panel-head-title">场景</div> |
| | | <div class="panel-head-sub">每个场景只会有一个已发布版本,诊断运行时直接读取它。</div> |
| | | </div> |
| | | <div class="panel-body" v-loading="loadingScenes"> |
| | | <div v-if="!scenes.length" class="empty-shell"> |
| | | <strong>暂无场景</strong> |
| | | <div>请先检查场景接口是否可用。</div> |
| | | </div> |
| | | <div v-else> |
| | | <div class="scene-card" |
| | | :class="{ active: selectedSceneCode === scene.code }" |
| | | v-for="scene in scenes" |
| | | :key="scene.code" |
| | | @click="selectScene(scene.code)"> |
| | | <div class="scene-head"> |
| | | <div> |
| | | <div style="font-size:14px;font-weight:700;color:#284059;">{{ scene.label }}</div> |
| | | <div class="scene-code">{{ scene.code }}</div> |
| | | </div> |
| | | <el-tag size="mini" :type="publishedTemplate(scene.code) ? 'success' : 'info'"> |
| | | {{ publishedTemplate(scene.code) ? ('v' + publishedTemplate(scene.code).version) : '未发布' }} |
| | | </el-tag> |
| | | </div> |
| | | <div class="scene-stats"> |
| | | <div class="mini-stat"> |
| | | <div class="k">版本数</div> |
| | | <div class="v">{{ templatesByScene(scene.code).length }}</div> |
| | | </div> |
| | | <div class="mini-stat"> |
| | | <div class="k">草稿数</div> |
| | | <div class="v">{{ draftCount(scene.code) }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="panel"> |
| | | <div class="panel-head"> |
| | | <div class="panel-head-title">版本列表</div> |
| | | <div class="panel-head-sub"> |
| | | 当前场景:{{ selectedSceneLabel }} |
| | | <span v-if="currentPublishedTemplate">,线上版本 v{{ currentPublishedTemplate.version }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="panel-body" v-loading="loadingTemplates"> |
| | | <div class="version-toolbar"> |
| | | <el-button type="primary" size="mini" @click="createBlankDraft" :disabled="!selectedSceneCode">新建草稿</el-button> |
| | | <el-button size="mini" @click="createDraftFromPublished" :disabled="!currentPublishedTemplate">基于已发布新建</el-button> |
| | | </div> |
| | | <div v-if="!selectedTemplates.length" class="empty-shell"> |
| | | <strong>当前场景还没有版本</strong> |
| | | <div>你可以直接新建草稿,或者先执行“补齐默认Prompt”。</div> |
| | | </div> |
| | | <div v-else> |
| | | <div class="version-card" |
| | | :class="{ active: isSelectedRow(row), published: row.published === 1 }" |
| | | v-for="row in selectedTemplates" |
| | | :key="rowKey(row)" |
| | | @click="openTemplate(row)"> |
| | | <div class="version-head"> |
| | | <div> |
| | | <div class="version-title">{{ displayTemplateTitle(row) }}</div> |
| | | <div class="version-meta"> |
| | | {{ displayTemplateVersion(row) }}<br /> |
| | | 更新时间:{{ formatDateTime(row.updateTime) }}<br /> |
| | | 备注:{{ row.memo || '-' }} |
| | | </div> |
| | | </div> |
| | | <div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;"> |
| | | <el-tag size="mini" type="info" v-if="row.__unsaved === true">未保存</el-tag> |
| | | <el-tag size="mini" :type="row.published === 1 ? 'warning' : 'info'"> |
| | | {{ row.published === 1 ? '已发布' : '草稿' }} |
| | | </el-tag> |
| | | <el-tag size="mini" :type="row.status === 1 ? 'success' : 'info'"> |
| | | {{ row.status === 1 ? '启用' : '禁用' }} |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | <div class="version-actions"> |
| | | <div class="version-actions-left"> |
| | | <el-button type="text" size="mini" @click.stop="openTemplate(row)">{{ row.__unsaved === true ? '继续编辑' : '查看' }}</el-button> |
| | | <el-button type="text" size="mini" @click.stop="cloneFromTemplate(row)">复制成草稿</el-button> |
| | | </div> |
| | | <div class="version-actions-right"> |
| | | <el-button type="text" size="mini" @click.stop="publishTemplate(row)" :disabled="row.published === 1">发布</el-button> |
| | | <el-button type="text" size="mini" @click.stop="cancelPublish(row)" v-if="row.published === 1">取消发布</el-button> |
| | | <el-button type="text" size="mini" style="color:#F56C6C;" @click.stop="deleteTemplate(row)" :disabled="row.published === 1">删除</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="panel editor-panel"> |
| | | <div class="panel-head"> |
| | | <div class="panel-head-title">编辑器</div> |
| | | <div class="panel-head-sub">{{ editorModeText }}</div> |
| | | </div> |
| | | <div class="panel-body" v-loading="saving"> |
| | | <div class="editor-toolbar"> |
| | | <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;"> |
| | | <el-tag size="mini" :type="editor.published === 1 ? 'warning' : 'info'"> |
| | | {{ editor.published === 1 ? '来源:已发布版本' : (editor.id ? '来源:草稿' : '来源:新建') }} |
| | | </el-tag> |
| | | <el-tag size="mini" :type="editor.status === 1 ? 'success' : 'info'"> |
| | | {{ editor.status === 1 ? '启用' : '禁用' }} |
| | | </el-tag> |
| | | </div> |
| | | <div class="editor-toolbar-right"> |
| | | <el-button size="mini" @click="resetEditor">清空编辑器</el-button> |
| | | <el-button type="primary" size="mini" @click="saveEditor(false)" :disabled="!editor.sceneCode">保存</el-button> |
| | | <el-button type="warning" size="mini" @click="publishTemplate()" :disabled="!editor.sceneCode">发布</el-button> |
| | | <el-button size="mini" @click="cancelPublish()" v-if="editor.published === 1 && editor.id">取消发布</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="editor-form"> |
| | | <div> |
| | | <div class="field-label">场景</div> |
| | | <el-select v-model="editor.sceneCode" size="mini" style="width:100%;" placeholder="请选择场景"> |
| | | <el-option v-for="scene in scenes" :key="scene.code" :label="scene.label + '(' + scene.code + ')'" :value="scene.code"></el-option> |
| | | </el-select> |
| | | </div> |
| | | <div> |
| | | <div class="field-label">Prompt名称</div> |
| | | <el-input v-model="editor.name" size="mini" placeholder="例如:WCS专家问答 v3"></el-input> |
| | | </div> |
| | | <div> |
| | | <div class="field-label">状态</div> |
| | | <el-switch v-model="editor.status" :active-value="1" :inactive-value="0"></el-switch> |
| | | </div> |
| | | <div> |
| | | <div class="field-label">备注</div> |
| | | <el-input v-model="editor.memo" size="mini" placeholder="记录这次优化目的或测试结论"></el-input> |
| | | </div> |
| | | <div class="editor-full"> |
| | | <div class="field-label">Prompt内容</div> |
| | | <el-input |
| | | class="editor-textarea" |
| | | type="textarea" |
| | | v-model="editor.content" |
| | | placeholder="在这里编辑 Prompt 内容" |
| | | :autosize="{ minRows: 20, maxRows: 28 }"></el-input> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="editor-stats"> |
| | | <div> |
| | | 字符数 {{ contentCharCount }} · 行数 {{ contentLineCount }} |
| | | </div> |
| | | <div> |
| | | 当前场景线上版本:{{ currentPublishedTemplate ? ('v' + currentPublishedTemplate.version + ' / ' + (currentPublishedTemplate.name || '-')) : '暂无' }} |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="editor-hint"> |
| | | <div v-if="editor.published === 1"> |
| | | 当前载入的是已发布版本。现在不允许直接保存修改,请先点击“取消发布”,再保存当前版本。 |
| | | </div> |
| | | <div v-else-if="editor.id"> |
| | | 当前正在编辑草稿 v{{ editor.version || '-' }}。保存只会更新这份草稿,发布后它会替换当前场景的线上版本。 |
| | | </div> |
| | | <div v-else> |
| | | 当前是新草稿。你一新建它就会出现在左侧版本列表里;可以先保存,再单独发布。 |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script> |
| | | new Vue({ |
| | | el: '#app', |
| | | data: function() { |
| | | return { |
| | | headerIcon: getAiIconHtml(34, 34), |
| | | loadingScenes: false, |
| | | loadingTemplates: false, |
| | | saving: false, |
| | | scenes: [], |
| | | templates: [], |
| | | selectedSceneCode: '', |
| | | selectedTemplateId: null, |
| | | editor: { |
| | | id: null, |
| | | name: '', |
| | | sceneCode: '', |
| | | version: null, |
| | | content: '', |
| | | status: 1, |
| | | published: 0, |
| | | memo: '', |
| | | publishedTime: null, |
| | | updateTime: null, |
| | | __localKey: null, |
| | | __unsaved: false |
| | | } |
| | | }; |
| | | }, |
| | | computed: { |
| | | summary: function() { |
| | | var published = 0; |
| | | var drafts = 0; |
| | | for (var i = 0; i < this.templates.length; i++) { |
| | | if (this.templates[i].published === 1) published++; |
| | | else drafts++; |
| | | } |
| | | return { |
| | | sceneCount: this.scenes.length, |
| | | totalTemplates: this.templates.length, |
| | | publishedTemplates: published, |
| | | draftTemplates: drafts |
| | | }; |
| | | }, |
| | | selectedSceneLabel: function() { |
| | | for (var i = 0; i < this.scenes.length; i++) { |
| | | if (this.scenes[i].code === this.selectedSceneCode) { |
| | | return this.scenes[i].label; |
| | | } |
| | | } |
| | | return this.selectedSceneCode || '未选择'; |
| | | }, |
| | | selectedTemplates: function() { |
| | | var list = this.templatesByScene(this.selectedSceneCode).slice().sort(function(a, b) { |
| | | var av = a && a.version ? a.version : 0; |
| | | var bv = b && b.version ? b.version : 0; |
| | | return bv - av; |
| | | }); |
| | | if (this.hasUnsavedEditorForScene(this.selectedSceneCode)) { |
| | | list.unshift(this.buildUnsavedTemplateCard()); |
| | | } |
| | | return list; |
| | | }, |
| | | currentPublishedTemplate: function() { |
| | | return this.publishedTemplate(this.selectedSceneCode); |
| | | }, |
| | | editorModeText: function() { |
| | | if (!this.editor.sceneCode) { |
| | | return '先从左侧选择场景,或者直接新建草稿。'; |
| | | } |
| | | if (this.editor.published === 1) { |
| | | return '已发布版本只作为查看和复制来源,不直接覆盖修改。'; |
| | | } |
| | | if (this.editor.id) { |
| | | return '正在编辑已有草稿,保存后不会影响线上版本,只有发布才会切换。'; |
| | | } |
| | | return '当前是新草稿,可以自由编辑并保存。'; |
| | | }, |
| | | contentCharCount: function() { |
| | | return this.editor.content ? this.editor.content.length : 0; |
| | | }, |
| | | contentLineCount: function() { |
| | | if (!this.editor.content) return 0; |
| | | return this.editor.content.split(/\r?\n/).length; |
| | | } |
| | | }, |
| | | methods: { |
| | | emptyEditor: function() { |
| | | return { |
| | | id: null, |
| | | name: '', |
| | | sceneCode: '', |
| | | version: null, |
| | | content: '', |
| | | status: 1, |
| | | published: 0, |
| | | memo: '', |
| | | publishedTime: null, |
| | | updateTime: null, |
| | | __localKey: null, |
| | | __unsaved: false |
| | | }; |
| | | }, |
| | | authHeaders: function() { |
| | | return { token: localStorage.getItem('token') }; |
| | | }, |
| | | requestJson: function(url, options) { |
| | | var self = this; |
| | | var opts = options || {}; |
| | | opts.headers = Object.assign({}, opts.headers || {}, self.authHeaders()); |
| | | return fetch(url, opts) |
| | | .then(function(resp) { return resp.json(); }) |
| | | .then(function(res) { |
| | | if (res && res.code === 403) { |
| | | top.location.href = baseUrl + '/'; |
| | | throw new Error('未授权'); |
| | | } |
| | | return res; |
| | | }); |
| | | }, |
| | | formatDateTime: function(input) { |
| | | if (!input) return '-'; |
| | | var d = input instanceof Date ? input : new Date(input); |
| | | if (isNaN(d.getTime())) return String(input); |
| | | var pad = function(n) { return n < 10 ? ('0' + n) : String(n); }; |
| | | return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' |
| | | + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); |
| | | }, |
| | | normalizeTemplate: function(item) { |
| | | return { |
| | | id: item && item.id != null ? item.id : null, |
| | | name: item && item.name ? item.name : '', |
| | | sceneCode: item && item.sceneCode ? item.sceneCode : '', |
| | | version: item && item.version != null ? item.version : null, |
| | | content: item && item.content ? item.content : '', |
| | | status: item && item.status != null ? item.status : 1, |
| | | published: item && item.published != null ? item.published : 0, |
| | | memo: item && item.memo ? item.memo : '', |
| | | publishedTime: item ? item.publishedTime : null, |
| | | updateTime: item ? item.updateTime : null, |
| | | __localKey: item && item.__localKey ? item.__localKey : null, |
| | | __unsaved: item && item.__unsaved === true |
| | | }; |
| | | }, |
| | | rowKey: function(row) { |
| | | if (!row) return 'row_empty'; |
| | | if (row.id != null) return 'db_' + row.id; |
| | | return row.__localKey || 'draft_local'; |
| | | }, |
| | | isSelectedRow: function(row) { |
| | | if (!row) return false; |
| | | if (row.id != null) { |
| | | return this.selectedTemplateId === row.id; |
| | | } |
| | | return row.__localKey && this.editor && this.editor.__localKey === row.__localKey; |
| | | }, |
| | | displayTemplateTitle: function(row) { |
| | | if (!row) return ''; |
| | | if (row.name) return row.name; |
| | | if (row.version != null) return this.selectedSceneLabel + ' v' + row.version; |
| | | return this.selectedSceneLabel + ' 未保存草稿'; |
| | | }, |
| | | displayTemplateVersion: function(row) { |
| | | if (!row) return '-'; |
| | | if (row.version != null) { |
| | | return '版本 v' + row.version; |
| | | } |
| | | return '版本号将在保存后生成'; |
| | | }, |
| | | buildLocalDraftKey: function() { |
| | | return 'draft_' + Date.now() + '_' + Math.floor(Math.random() * 100000); |
| | | }, |
| | | hasUnsavedEditorForScene: function(sceneCode) { |
| | | return !!(this.editor |
| | | && this.editor.sceneCode |
| | | && this.editor.sceneCode === sceneCode |
| | | && this.editor.id == null |
| | | && this.editor.__unsaved === true); |
| | | }, |
| | | buildUnsavedTemplateCard: function() { |
| | | return this.normalizeTemplate(this.editor); |
| | | }, |
| | | templatesByScene: function(sceneCode) { |
| | | var code = sceneCode || ''; |
| | | return this.templates.filter(function(x) { |
| | | return x.sceneCode === code; |
| | | }); |
| | | }, |
| | | publishedTemplate: function(sceneCode) { |
| | | var list = this.templatesByScene(sceneCode); |
| | | for (var i = 0; i < list.length; i++) { |
| | | if (list[i].published === 1 && list[i].status === 1) { |
| | | return list[i]; |
| | | } |
| | | } |
| | | for (var j = 0; j < list.length; j++) { |
| | | if (list[j].published === 1) { |
| | | return list[j]; |
| | | } |
| | | } |
| | | return null; |
| | | }, |
| | | draftCount: function(sceneCode) { |
| | | var list = this.templatesByScene(sceneCode); |
| | | var count = 0; |
| | | for (var i = 0; i < list.length; i++) { |
| | | if (list[i].published !== 1) count++; |
| | | } |
| | | return count; |
| | | }, |
| | | goLlmConfig: function() { |
| | | window.location.href = 'llm_config.html'; |
| | | }, |
| | | reloadAll: function() { |
| | | var self = this; |
| | | self.loadScenes().then(function() { |
| | | return self.loadTemplates(); |
| | | }); |
| | | }, |
| | | loadScenes: function() { |
| | | var self = this; |
| | | self.loadingScenes = true; |
| | | return self.requestJson(baseUrl + '/ai/prompt/template/sceneList/auth') |
| | | .then(function(res) { |
| | | self.loadingScenes = false; |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '场景加载失败'); |
| | | return; |
| | | } |
| | | self.scenes = Array.isArray(res.data) ? res.data : []; |
| | | if (!self.selectedSceneCode && self.scenes.length) { |
| | | self.selectedSceneCode = self.scenes[0].code; |
| | | } |
| | | }) |
| | | .catch(function() { |
| | | self.loadingScenes = false; |
| | | self.$message.error('场景加载失败'); |
| | | }); |
| | | }, |
| | | loadTemplates: function() { |
| | | var self = this; |
| | | self.loadingTemplates = true; |
| | | return self.requestJson(baseUrl + '/ai/prompt/template/list/auth') |
| | | .then(function(res) { |
| | | self.loadingTemplates = false; |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '版本加载失败'); |
| | | return; |
| | | } |
| | | var rows = Array.isArray(res.data) ? res.data : []; |
| | | self.templates = rows.map(self.normalizeTemplate); |
| | | self.syncSelectionAfterReload(); |
| | | }) |
| | | .catch(function() { |
| | | self.loadingTemplates = false; |
| | | self.$message.error('版本加载失败'); |
| | | }); |
| | | }, |
| | | syncSelectionAfterReload: function() { |
| | | if (!this.selectedSceneCode && this.scenes.length) { |
| | | this.selectedSceneCode = this.scenes[0].code; |
| | | } |
| | | if (!this.selectedSceneCode) { |
| | | this.resetEditor(); |
| | | return; |
| | | } |
| | | if (this.selectedTemplateId) { |
| | | for (var i = 0; i < this.templates.length; i++) { |
| | | if (this.templates[i].id === this.selectedTemplateId) { |
| | | this.editor = this.normalizeTemplate(this.templates[i]); |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | if (this.editor && !this.editor.id && this.editor.sceneCode === this.selectedSceneCode) { |
| | | return; |
| | | } |
| | | this.resetEditor(); |
| | | }, |
| | | selectScene: function(sceneCode) { |
| | | this.selectedSceneCode = sceneCode; |
| | | this.selectedTemplateId = null; |
| | | this.resetEditor(); |
| | | }, |
| | | openTemplate: function(row) { |
| | | if (!row) return; |
| | | if (row.id != null) { |
| | | this.selectedTemplateId = row.id; |
| | | this.editor = this.normalizeTemplate(row); |
| | | return; |
| | | } |
| | | this.selectedTemplateId = null; |
| | | this.editor = this.normalizeTemplate(row); |
| | | }, |
| | | resetEditor: function() { |
| | | var editor = this.emptyEditor(); |
| | | editor.sceneCode = this.selectedSceneCode || ''; |
| | | editor.status = 1; |
| | | this.editor = editor; |
| | | this.selectedTemplateId = null; |
| | | }, |
| | | createBlankDraft: function() { |
| | | if (!this.selectedSceneCode) { |
| | | this.$message.warning('请先选择场景'); |
| | | return; |
| | | } |
| | | this.selectedTemplateId = null; |
| | | this.resetEditor(); |
| | | this.editor.name = this.selectedSceneLabel + ' 新草稿'; |
| | | this.editor.__localKey = this.buildLocalDraftKey(); |
| | | this.editor.__unsaved = true; |
| | | this.editor.updateTime = new Date(); |
| | | }, |
| | | cloneFromTemplate: function(row) { |
| | | if (!row) return; |
| | | this.selectedSceneCode = row.sceneCode; |
| | | this.selectedTemplateId = null; |
| | | this.editor = this.normalizeTemplate(row); |
| | | this.editor.id = null; |
| | | this.editor.published = 0; |
| | | this.editor.version = null; |
| | | this.editor.name = (row.name || this.selectedSceneLabel) + ' - 草稿'; |
| | | this.editor.__localKey = this.buildLocalDraftKey(); |
| | | this.editor.__unsaved = true; |
| | | this.editor.updateTime = new Date(); |
| | | }, |
| | | createDraftFromPublished: function() { |
| | | if (!this.currentPublishedTemplate) { |
| | | this.$message.warning('当前场景暂无已发布版本'); |
| | | return; |
| | | } |
| | | this.cloneFromTemplate(this.currentPublishedTemplate); |
| | | }, |
| | | buildSavePayload: function() { |
| | | var payloadId = this.editor.id; |
| | | if (this.editor.published === 1) { |
| | | payloadId = null; |
| | | } |
| | | return { |
| | | id: payloadId, |
| | | name: this.editor.name, |
| | | sceneCode: this.editor.sceneCode, |
| | | content: this.editor.content, |
| | | status: this.editor.status, |
| | | memo: this.editor.memo |
| | | }; |
| | | }, |
| | | saveEditor: function(publishAfterSave) { |
| | | var self = this; |
| | | if (self.editor.published === 1) { |
| | | self.$alert('当前版本已经发布,请先取消发布后再保存。', '提示', { |
| | | confirmButtonText: '知道了', |
| | | type: 'warning' |
| | | }); |
| | | return; |
| | | } |
| | | if (!self.editor.sceneCode) { |
| | | self.$message.warning('请选择场景'); |
| | | return; |
| | | } |
| | | if (!self.editor.content || !self.editor.content.trim()) { |
| | | self.$message.warning('Prompt内容不能为空'); |
| | | return; |
| | | } |
| | | self.saving = true; |
| | | self.requestJson(baseUrl + '/ai/prompt/template/save/auth', { |
| | | method: 'POST', |
| | | headers: { 'Content-Type': 'application/json' }, |
| | | body: JSON.stringify(self.buildSavePayload()) |
| | | }) |
| | | .then(function(res) { |
| | | if (!res || res.code !== 200) { |
| | | self.saving = false; |
| | | self.$message.error((res && res.msg) ? res.msg : '保存失败'); |
| | | return; |
| | | } |
| | | var saved = self.normalizeTemplate(res.data || {}); |
| | | self.selectedSceneCode = saved.sceneCode; |
| | | self.selectedTemplateId = saved.id; |
| | | self.editor = saved; |
| | | if (publishAfterSave === true && saved.id) { |
| | | self.publishTemplate(saved, true); |
| | | return; |
| | | } |
| | | self.saving = false; |
| | | self.$message.success('保存成功'); |
| | | self.loadTemplates(); |
| | | }) |
| | | .catch(function() { |
| | | self.saving = false; |
| | | self.$message.error('保存失败'); |
| | | }); |
| | | }, |
| | | publishTemplate: function(row, silentAfterSave) { |
| | | var self = this; |
| | | var target = row || self.editor; |
| | | if (target && target.published === 1) { |
| | | self.$message.warning('当前版本已经是已发布状态'); |
| | | return; |
| | | } |
| | | if (!target || !target.id) { |
| | | self.saveEditor(true); |
| | | return; |
| | | } |
| | | self.requestJson(baseUrl + '/ai/prompt/template/publish/auth?id=' + encodeURIComponent(target.id), { |
| | | method: 'POST' |
| | | }) |
| | | .then(function(res) { |
| | | self.saving = false; |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '发布失败'); |
| | | return; |
| | | } |
| | | var published = self.normalizeTemplate(res.data || {}); |
| | | self.selectedSceneCode = published.sceneCode; |
| | | self.selectedTemplateId = published.id; |
| | | self.editor = published; |
| | | if (!silentAfterSave) { |
| | | self.$message.success('已发布到当前场景'); |
| | | } else { |
| | | self.$message.success('保存并发布成功'); |
| | | } |
| | | self.loadTemplates(); |
| | | }) |
| | | .catch(function() { |
| | | self.saving = false; |
| | | self.$message.error('发布失败'); |
| | | }); |
| | | }, |
| | | cancelPublish: function(row) { |
| | | var self = this; |
| | | var target = row || self.editor; |
| | | if (!target || !target.id) { |
| | | self.$message.warning('当前没有可取消发布的版本'); |
| | | return; |
| | | } |
| | | if (target.published !== 1) { |
| | | self.$message.warning('当前版本不是已发布状态'); |
| | | return; |
| | | } |
| | | self.$confirm('取消发布后,该场景会暂时没有线上 Prompt,直到你重新发布一个版本。确定继续吗?', '提示', { |
| | | type: 'warning', |
| | | closeOnClickModal: false |
| | | }).then(function() { |
| | | self.requestJson(baseUrl + '/ai/prompt/template/cancelPublish/auth?id=' + encodeURIComponent(target.id), { |
| | | method: 'POST' |
| | | }) |
| | | .then(function(res) { |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '取消发布失败'); |
| | | return; |
| | | } |
| | | var changed = self.normalizeTemplate(res.data || {}); |
| | | if (self.editor && self.editor.id === changed.id) { |
| | | self.selectedTemplateId = changed.id; |
| | | self.editor = changed; |
| | | } |
| | | self.$message.success('已取消发布,现在可以保存当前版本了'); |
| | | self.loadTemplates(); |
| | | }) |
| | | .catch(function() { |
| | | self.$message.error('取消发布失败'); |
| | | }); |
| | | }).catch(function() {}); |
| | | }, |
| | | deleteTemplate: function(row) { |
| | | var self = this; |
| | | if (!row) return; |
| | | if (!row.id) { |
| | | if (row.__localKey && self.editor && self.editor.__localKey === row.__localKey) { |
| | | self.resetEditor(); |
| | | self.$message.success('未保存草稿已移除'); |
| | | } |
| | | return; |
| | | } |
| | | if (row.published === 1) { |
| | | self.$message.warning('已发布版本不能删除'); |
| | | return; |
| | | } |
| | | self.$confirm('确定删除这个草稿版本吗?', '提示', { type: 'warning' }).then(function() { |
| | | self.requestJson(baseUrl + '/ai/prompt/template/delete/auth?id=' + encodeURIComponent(row.id), { |
| | | method: 'POST' |
| | | }) |
| | | .then(function(res) { |
| | | if (res && res.code === 200) { |
| | | if (self.selectedTemplateId === row.id) { |
| | | self.selectedTemplateId = null; |
| | | self.resetEditor(); |
| | | } |
| | | self.$message.success('删除成功'); |
| | | self.loadTemplates(); |
| | | } else { |
| | | self.$message.error((res && res.msg) ? res.msg : '删除失败'); |
| | | } |
| | | }) |
| | | .catch(function() { |
| | | self.$message.error('删除失败'); |
| | | }); |
| | | }).catch(function() {}); |
| | | }, |
| | | restoreDefaults: function() { |
| | | var self = this; |
| | | self.requestJson(baseUrl + '/ai/prompt/template/initDefaults/auth', { |
| | | method: 'POST' |
| | | }) |
| | | .then(function(res) { |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '补齐失败'); |
| | | return; |
| | | } |
| | | self.$message.success('默认Prompt已检查并补齐'); |
| | | self.reloadAll(); |
| | | }) |
| | | .catch(function() { |
| | | self.$message.error('补齐失败'); |
| | | }); |
| | | } |
| | | }, |
| | | mounted: function() { |
| | | var self = this; |
| | | if (window.WCS_I18N && typeof window.WCS_I18N.onReady === 'function') { |
| | | window.WCS_I18N.onReady(function() { |
| | | self.$forceUpdate(); |
| | | }); |
| | | } |
| | | self.reloadAll(); |
| | | } |
| | | }); |
| | | </script> |
| | | </body> |
| | | </html> |