From f0b4c2807c463698719d2f3de037f2225118cf9a Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期四, 12 三月 2026 14:22:46 +0800
Subject: [PATCH] #

---
 src/main/webapp/views/ai/prompt_config.html                           | 1084 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main/java/com/zy/ai/service/impl/AiPromptTemplateServiceImpl.java |   30 +
 src/main/java/com/zy/ai/controller/AiPromptTemplateController.java    |   10 
 src/main/webapp/views/ai/llm_config.html                              |    4 
 src/main/java/com/zy/ai/service/AiPromptTemplateService.java          |    2 
 src/main/resources/sql/20260312_add_ai_prompt_menu.sql                |   42 ++
 6 files changed, 1,168 insertions(+), 4 deletions(-)

diff --git a/src/main/java/com/zy/ai/controller/AiPromptTemplateController.java b/src/main/java/com/zy/ai/controller/AiPromptTemplateController.java
index 7e919c5..35566e9 100644
--- a/src/main/java/com/zy/ai/controller/AiPromptTemplateController.java
+++ b/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) {
diff --git a/src/main/java/com/zy/ai/service/AiPromptTemplateService.java b/src/main/java/com/zy/ai/service/AiPromptTemplateService.java
index e269966..f2a24f7 100644
--- a/src/main/java/com/zy/ai/service/AiPromptTemplateService.java
+++ b/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();
diff --git a/src/main/java/com/zy/ai/service/impl/AiPromptTemplateServiceImpl.java b/src/main/java/com/zy/ai/service/impl/AiPromptTemplateServiceImpl.java
index fedfd7f..004d3fb 100644
--- a/src/main/java/com/zy/ai/service/impl/AiPromptTemplateServiceImpl.java
+++ b/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锛宻ceneCode=" + scene.getCode());
+            throw new IllegalStateException("褰撳墠鍦烘櫙娌℃湁宸插彂甯� Prompt锛宻ceneCode=" + 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++;
             }
diff --git a/src/main/resources/sql/20260312_add_ai_prompt_menu.sql b/src/main/resources/sql/20260312_add_ai_prompt_menu.sql
new file mode 100644
index 0000000..7d6aa10
--- /dev/null
+++ b/src/main/resources/sql/20260312_add_ai_prompt_menu.sql
@@ -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');
diff --git a/src/main/webapp/views/ai/llm_config.html b/src/main/webapp/views/ai/llm_config.html
index 8d91b18..9ceff36 100644
--- a/src/main/webapp/views/ai/llm_config.html
+++ b/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;
diff --git a/src/main/webapp/views/ai/prompt_config.html b/src/main/webapp/views/ai/prompt_config.html
new file mode 100644
index 0000000..fc91051
--- /dev/null
+++ b/src/main/webapp/views/ai/prompt_config.html
@@ -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>璇峰厛妫�鏌ュ満鏅帴鍙f槸鍚﹀彲鐢ㄣ��</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>浣犲彲浠ョ洿鎺ユ柊寤鸿崏绋匡紝鎴栬�呭厛鎵ц鈥滆ˉ榻愰粯璁rompt鈥濄��</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="渚嬪锛歐CS涓撳闂瓟 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">
+            褰撳墠姝e湪缂栬緫鑽夌 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 '姝e湪缂栬緫宸叉湁鑽夌锛屼繚瀛樺悗涓嶄細褰卞搷绾夸笂鐗堟湰锛屽彧鏈夊彂甯冩墠浼氬垏鎹€��';
+        }
+        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>

--
Gitblit v1.9.1