#
Junjie
3 天以前 f0b4c2807c463698719d2f3de037f2225118cf9a
#
4个文件已修改
2个文件已添加
1172 ■■■■■ 已修改文件
src/main/java/com/zy/ai/controller/AiPromptTemplateController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/AiPromptTemplateService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/impl/AiPromptTemplateServiceImpl.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260312_add_ai_prompt_menu.sql 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/ai/llm_config.html 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/ai/prompt_config.html 1084 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/controller/AiPromptTemplateController.java
@@ -79,6 +79,16 @@
        }
    }
    @PostMapping("/cancelPublish/auth")
    @ManagerAuth
    public R cancelPublish(@RequestParam("id") Long id) {
        try {
            return R.ok(aiPromptTemplateService.cancelPublish(id, getUserId()));
        } catch (IllegalArgumentException e) {
            return R.error(e.getMessage());
        }
    }
    @PostMapping("/delete/auth")
    @ManagerAuth
    public R delete(@RequestParam("id") Long id) {
src/main/java/com/zy/ai/service/AiPromptTemplateService.java
@@ -14,6 +14,8 @@
    AiPromptTemplate publishPrompt(Long id, Long operatorUserId);
    AiPromptTemplate cancelPublish(Long id, Long operatorUserId);
    boolean deletePrompt(Long id);
    int initDefaultsIfMissing();
src/main/java/com/zy/ai/service/impl/AiPromptTemplateServiceImpl.java
@@ -34,13 +34,15 @@
            synchronized (("ai_prompt_scene_init_" + scene.getCode()).intern()) {
                prompt = findPublished(scene.getCode());
                if (prompt == null) {
                    prompt = ensurePublishedScene(scene);
                    if (findLatest(scene.getCode()) == null) {
                        prompt = ensurePublishedScene(scene);
                    }
                }
            }
        }
        if (prompt == null) {
            throw new IllegalStateException("未找到已发布的 Prompt,sceneCode=" + scene.getCode());
            throw new IllegalStateException("当前场景没有已发布 Prompt,sceneCode=" + scene.getCode());
        }
        return prompt;
    }
@@ -126,6 +128,26 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AiPromptTemplate cancelPublish(Long id, Long operatorUserId) {
        if (id == null) {
            throw new IllegalArgumentException("id 不能为空");
        }
        AiPromptTemplate db = this.getById(id);
        if (db == null) {
            throw new IllegalArgumentException("Prompt 不存在");
        }
        if (!Short.valueOf((short) 1).equals(db.getPublished())) {
            throw new IllegalArgumentException("当前 Prompt 不是已发布状态");
        }
        db.setPublished((short) 0);
        db.setPublishedBy(operatorUserId);
        db.setPublishedTime(null);
        this.updateById(db);
        return db;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deletePrompt(Long id) {
        if (id == null) {
            return false;
@@ -145,8 +167,8 @@
    public int initDefaultsIfMissing() {
        int changed = 0;
        for (AiPromptScene scene : AiPromptScene.values()) {
            AiPromptTemplate prompt = findPublished(scene.getCode());
            if (prompt == null) {
            AiPromptTemplate latest = findLatest(scene.getCode());
            if (latest == null) {
                ensurePublishedScene(scene);
                changed++;
            }
src/main/resources/sql/20260312_add_ai_prompt_menu.sql
New file
@@ -0,0 +1,42 @@
-- 将 Prompt配置 菜单挂载到:开发专用 -> Prompt配置
-- 执行后请在“角色授权”里给对应角色勾选新菜单。
SET @dev_parent_id := (
  SELECT id
  FROM sys_resource
  WHERE name = '开发专用' AND level = 1
  ORDER BY id
  LIMIT 1
);
INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
SELECT 'ai/prompt_config.html', 'Prompt配置', @dev_parent_id, 2, 1000, 1
FROM dual
WHERE @dev_parent_id IS NOT NULL
  AND NOT EXISTS (
    SELECT 1
    FROM sys_resource
    WHERE code = 'ai/prompt_config.html' AND level = 2
  );
SET @ai_prompt_id := (
  SELECT id
  FROM sys_resource
  WHERE code = 'ai/prompt_config.html' AND level = 2
  ORDER BY id
  LIMIT 1
);
INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
SELECT 'ai/prompt_config.html#view', '查看', @ai_prompt_id, 3, 1, 1
FROM dual
WHERE @ai_prompt_id IS NOT NULL
  AND NOT EXISTS (
    SELECT 1
    FROM sys_resource
    WHERE code = 'ai/prompt_config.html#view' AND level = 3
  );
SELECT id, code, name, resource_id, level, sort, status
FROM sys_resource
WHERE code IN ('ai/prompt_config.html', 'ai/prompt_config.html#view');
src/main/webapp/views/ai/llm_config.html
@@ -268,6 +268,7 @@
        </div>
      </div>
      <div class="hero-actions">
        <el-button size="mini" @click="goPromptCenter">Prompt配置</el-button>
        <el-button type="primary" size="mini" @click="addRoute">新增路由</el-button>
        <el-button size="mini" @click="exportRoutes">导出JSON</el-button>
        <el-button size="mini" @click="triggerImport">导入JSON</el-button>
@@ -534,6 +535,9 @@
        var value = route && route.name ? String(route.name) : '';
        return this.translateLegacyText(value);
      },
      goPromptCenter: function() {
        window.location.href = 'prompt_config.html';
      },
      handleRouteNameInput: function(route, value) {
        if (!route) {
          return;
src/main/webapp/views/ai/prompt_config.html
New file
@@ -0,0 +1,1084 @@
<!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>