Files
mp_1787300862_s8r25x 1ef034cd59 init: 旅行商品发布AI助手Demo
- 单文件交互式Demo:六大表单模块 + 右侧AI助手面板
- 一键发品(弹窗式)与对话式自主发品(Agent模式)
- 多工具串行调度 + 逐工具确认(human-in-the-loop)
- 偏好学习机制(localStorage,越用越好用)
- Prompt提示词设计文档与项目方法论笔记
2026-08-27 17:27:20 +08:00

1612 lines
80 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品发布 - 旅智工作台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
background: #f0f2f5; color: #1f2329;
height: 100vh; overflow: hidden;
display: flex; flex-direction: column;
}
/* ============ 顶部导航 ============ */
.topbar {
height: 52px; background: #fff; border-bottom: 1px solid #e5e6eb;
display: flex; align-items: center; padding: 0 20px; flex-shrink: 0; z-index: 10;
}
.topbar .logo { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 15px; color: #d96a00; }
.topbar .logo .icon {
width: 26px; height: 26px; background: linear-gradient(135deg, #ff7a1a, #ffb300);
border-radius: 7px; display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 14px;
}
.topbar .breadcrumb { margin-left: 24px; font-size: 13px; color: #86909c; }
.topbar .breadcrumb b { color: #1f2329; font-weight: 500; }
.topbar .right { margin-left: auto; display: flex; align-items: center; gap: 16px; font-size: 13px; color: #4e5969; }
.avatar-sm {
width: 30px; height: 30px; border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #8b5cf6); color: #fff;
display: flex; align-items: center; justify-content: center; font-size: 12px;
}
/* ============ 主体布局 ============ */
.main { flex: 1; display: flex; overflow: hidden; }
/* ============ 左侧:表单区 ============ */
.form-area { flex: 1; overflow-y: auto; padding: 20px 24px 60px; }
.page-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.page-head h1 { font-size: 20px; font-weight: 600; }
.page-head .sub { font-size: 13px; color: #86909c; margin-top: 4px; }
.btn-primary {
background: linear-gradient(135deg, #ff7a1a, #ff6600); color: #fff; border: none;
padding: 9px 22px; border-radius: 8px; font-size: 14px; cursor: pointer;
font-weight: 500; transition: opacity .2s;
}
.btn-primary:hover { opacity: .88; }
.card {
background: #fff; border-radius: 12px; border: 1px solid #e5e6eb;
padding: 24px; margin-bottom: 16px;
}
.card h2 { font-size: 15px; font-weight: 600; margin-bottom: 18px; display: flex; align-items: center; gap: 8px; }
.card h2 .dot { width: 4px; height: 14px; background: #ff7a1a; border-radius: 2px; }
.card h2 .badge-step {
margin-left: auto; font-size: 11px; font-weight: 500; color: #86909c;
background: #f7f8fa; border-radius: 12px; padding: 3px 10px;
}
/* 表单模块内嵌 AI 按钮 */
.ai-inline-btn {
margin-left: 8px; font-size: 12px; font-weight: 500;
color: #ff7a1a; background: #fff; border: 1px solid rgba(255, 122, 26, 0.5);
border-radius: 12px; padding: 3px 10px; cursor: pointer;
display: inline-flex; align-items: center; gap: 3px;
transition: all .15s ease; white-space: nowrap;
}
.ai-inline-btn:hover {
background: #ff7a1a; color: #fff; border-color: #ff7a1a;
box-shadow: 0 2px 8px rgba(255, 122, 26, 0.35);
}
.form-row { display: flex; gap: 16px; margin-bottom: 16px; }
.form-item { flex: 1; }
.form-item:last-child { margin-bottom: 0; }
.form-item label { display: block; font-size: 13px; color: #4e5969; margin-bottom: 6px; }
.form-item label .req { color: #f53f3f; margin-right: 2px; }
.form-item input, .form-item select, .form-item textarea {
width: 100%; border: 1px solid #e5e6eb; border-radius: 8px; padding: 9px 12px;
font-size: 13px; background: #fff; transition: border-color .2s, box-shadow .2s; font-family: inherit;
}
.form-item input:focus, .form-item select:focus, .form-item textarea:focus {
outline: none; border-color: #ff7a1a; box-shadow: 0 0 0 3px rgba(255,122,26,.12);
}
.form-item textarea { resize: vertical; min-height: 92px; line-height: 1.7; }
.form-item .hint { font-size: 12px; color: #c9cdd4; margin-top: 4px; }
.counter { font-size: 12px; color: #c9cdd4; margin-top: 4px; text-align: right; }
.field-highlight { border-color: #ff7a1a !important; box-shadow: 0 0 0 3px rgba(255,122,26,.18) !important; }
.section-highlight { outline: 2px solid #ff7a1a; outline-offset: 2px; border-radius: 12px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
border: 1px solid #e5e6eb; border-radius: 16px; padding: 5px 14px; font-size: 12px;
color: #4e5969; cursor: pointer; transition: all .2s; background: #fff;
}
.chip:hover { border-color: #ff7a1a; color: #d96a00; }
.chip.active { background: rgba(255,122,26,.08); border-color: #ff7a1a; color: #d96a00; }
/* 富文本编辑器 */
.rich-editor { border: 1px solid #e5e6eb; border-radius: 8px; overflow: hidden; }
.rich-toolbar {
display: flex; gap: 2px; padding: 6px 8px; border-bottom: 1px solid #f2f3f5; background: #fafbfc;
}
.rich-toolbar button {
border: none; background: transparent; width: 28px; height: 26px; border-radius: 6px;
cursor: pointer; font-size: 13px; color: #4e5969;
}
.rich-toolbar button:hover { background: #eef1f4; }
.rich-toolbar .sep { width: 1px; background: #e5e6eb; margin: 3px 4px; }
.rich-content {
min-height: 150px; padding: 12px 14px; font-size: 13px; line-height: 1.9; outline: none;
}
.rich-content:empty::before { content: attr(data-placeholder); color: #c9cdd4; }
.rich-content h3 { font-size: 14px; margin: 10px 0 6px; color: #d96a00; }
.rich-content ul { padding-left: 20px; }
.rich-content li { margin-bottom: 4px; }
.rich-content .img-slot {
display: flex; align-items: center; justify-content: center; gap: 6px;
height: 60px; border-radius: 8px; margin: 8px 0; font-size: 12px; color: #86909c;
background: repeating-linear-gradient(45deg, #f7f8fa, #f7f8fa 10px, #f2f3f5 10px, #f2f3f5 20px);
border: 1px dashed #d5d9e0;
}
/* 图片上传 */
.img-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.img-tile {
aspect-ratio: 4/3; border-radius: 10px; position: relative; overflow: hidden;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 6px; color: #fff; font-size: 13px; cursor: default;
}
.img-tile .em { font-size: 26px; }
.img-tile .tag {
position: absolute; top: 6px; left: 6px; font-size: 10px; background: rgba(0,0,0,.35);
padding: 2px 7px; border-radius: 10px; backdrop-filter: blur(2px);
}
.img-tile .ai-tag {
position: absolute; top: 6px; right: 6px; font-size: 10px; background: rgba(255,122,26,.9);
padding: 2px 7px; border-radius: 10px;
}
.img-tile .del {
position: absolute; bottom: 6px; right: 6px; width: 20px; height: 20px; border-radius: 50%;
background: rgba(0,0,0,.4); color: #fff; border: none; font-size: 11px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.img-upload {
aspect-ratio: 4/3; border: 1.5px dashed #c9cdd4; border-radius: 10px; display: flex;
flex-direction: column; align-items: center; justify-content: center; gap: 6px;
color: #86909c; font-size: 12px; cursor: pointer; transition: all .2s; background: #fafbfc;
}
.img-upload:hover { border-color: #ff7a1a; color: #d96a00; }
.img-upload .plus { font-size: 24px; color: #c9cdd4; }
/* 行程编辑 */
.day-card { border: 1px solid #e5e6eb; border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
.day-head {
display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: #fafbfc;
border-bottom: 1px solid #f2f3f5;
}
.day-head .dnum {
background: linear-gradient(135deg, #ff7a1a, #ffb300); color: #fff; font-size: 12px;
font-weight: 600; border-radius: 6px; padding: 3px 10px; flex-shrink: 0;
}
.day-head input {
flex: 1; border: none; outline: none; font-size: 13px; background: transparent; font-family: inherit;
}
.day-head .del-day {
border: none; background: transparent; color: #c9cdd4; cursor: pointer; font-size: 14px;
}
.day-head .del-day:hover { color: #f53f3f; }
.day-body { padding: 10px 12px; }
.day-body textarea {
width: 100%; border: none; outline: none; resize: vertical; font-size: 12px; line-height: 1.7;
min-height: 44px; font-family: inherit; background: transparent; color: #4e5969;
}
.add-day {
width: 100%; border: 1.5px dashed #c9cdd4; background: #fafbfc; border-radius: 10px;
padding: 10px; color: #86909c; font-size: 13px; cursor: pointer; transition: all .2s;
}
.add-day:hover { border-color: #ff7a1a; color: #d96a00; }
/* 价格库存表 */
.price-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.price-table th {
text-align: left; padding: 8px 10px; background: #fafbfc; color: #86909c;
font-weight: 500; font-size: 12px; border-bottom: 1px solid #f2f3f5;
}
.price-table td { padding: 6px 8px; border-bottom: 1px solid #f7f8fa; }
.price-table input {
width: 100%; border: 1px solid transparent; border-radius: 6px; padding: 6px 8px;
font-size: 13px; font-family: inherit; background: transparent;
}
.price-table input:hover, .price-table input:focus { border-color: #e5e6eb; background: #fff; outline: none; }
.price-table input:focus { border-color: #ff7a1a; }
.stock-low { color: #b45309; }
.status-pill { font-size: 11px; border-radius: 10px; padding: 2px 9px; }
.status-on { background: rgba(34,197,94,.1); color: #15803d; }
.status-off { background: #f2f3f5; color: #86909c; }
.del-row { border: none; background: transparent; color: #c9cdd4; cursor: pointer; font-size: 14px; }
.del-row:hover { color: #f53f3f; }
.empty-tip { padding: 18px; text-align: center; color: #c9cdd4; font-size: 13px; }
/* ============ 右侧:AI 助手 ============ */
.ai-panel {
width: 400px; background: #fff; border-left: 1px solid #e5e6eb;
display: flex; flex-direction: column; position: relative;
transition: margin-right .35s ease; flex-shrink: 0;
}
.ai-panel.collapsed { margin-right: -400px; }
.ai-header {
padding: 14px 16px; border-bottom: 1px solid #f2f3f5; display: flex; align-items: center; gap: 10px;
background: linear-gradient(135deg, rgba(255,122,26,.05), rgba(255,178,0,.05)); flex-shrink: 0;
}
.ai-avatar {
width: 38px; height: 38px; border-radius: 10px;
background: linear-gradient(135deg, #ff7a1a, #ffb300);
display: flex; align-items: center; justify-content: center; color: #fff; font-size: 18px;
position: relative; flex-shrink: 0;
}
.ai-avatar::after {
content: ""; position: absolute; right: -2px; bottom: -2px; width: 10px; height: 10px;
background: #22c55e; border: 2px solid #fff; border-radius: 50%;
}
.ai-header .info { flex: 1; }
.ai-header .name { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
.ai-header .name .badge {
font-size: 10px; background: rgba(255,122,26,.1); color: #d96a00;
padding: 2px 6px; border-radius: 4px; font-weight: 500;
}
.ai-header .status { font-size: 12px; color: #86909c; margin-top: 2px; }
.icon-btn {
width: 30px; height: 30px; border: 1px solid #e5e6eb; background: #fff; border-radius: 8px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
color: #86909c; font-size: 14px; transition: all .2s;
}
.icon-btn:hover { color: #d96a00; border-color: #ff7a1a; }
.ai-quick { padding: 12px 14px 4px; border-bottom: 1px solid #f2f3f5; flex-shrink: 0; }
.ai-quick .title { font-size: 12px; color: #86909c; margin-bottom: 8px; }
.quick-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding-bottom: 10px; }
.quick-btn {
border: 1px solid #e5e6eb; border-radius: 10px; background: #fff; padding: 9px 2px;
cursor: pointer; text-align: center; transition: all .2s;
display: flex; flex-direction: column; align-items: center; gap: 5px;
}
.quick-btn .qicon { width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 15px; }
.quick-btn .qtext { font-size: 12px; color: #4e5969; }
.quick-btn:hover { border-color: #ff7a1a; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(255,122,26,.12); }
.q1 { background: rgba(255,122,26,.1); }
.q2 { background: rgba(255,178,0,.1); }
.q3 { background: rgba(245,158,11,.12); }
.q4 { background: rgba(139,92,246,.1); }
.q5 { background: rgba(236,72,153,.08); }
.q6 { background: rgba(34,197,94,.1); }
.q7 { background: rgba(99,102,241,.1); }
.ai-messages { flex: 1; overflow-y: auto; padding: 16px 14px; display: flex; flex-direction: column; gap: 14px; }
.msg { display: flex; gap: 8px; max-width: 100%; animation: msgIn .3s ease; }
@keyframes msgIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.msg .m-avatar {
width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center; font-size: 13px; color: #fff;
}
.msg.ai .m-avatar { background: linear-gradient(135deg, #ff7a1a, #ffb300); }
.msg.user { flex-direction: row-reverse; }
.msg.user .m-avatar { background: linear-gradient(135deg, #6366f1, #8b5cf6); }
.msg .bubble {
background: #f7f8fa; border-radius: 12px; padding: 10px 12px; font-size: 13px;
line-height: 1.75; color: #1f2329; max-width: calc(100% - 40px); word-break: break-word;
}
.msg.user .bubble { background: linear-gradient(135deg, #ff7a1a, #ff6600); color: #fff; }
.bubble b { font-weight: 600; }
.bubble .hl { color: #d96a00; font-weight: 600; }
.gen-card {
background: #fff; border: 1px solid #ffe3c2; border-radius: 12px; padding: 12px;
margin-top: 8px; box-shadow: 0 2px 8px rgba(15,23,42,.04);
}
.gen-card .gc-head { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #d96a00; font-weight: 600; margin-bottom: 8px; }
.gen-card .gc-body { font-size: 13px; line-height: 1.8; color: #1f2329; background: #f8fafb; border-radius: 8px; padding: 10px 12px; max-height: 260px; overflow-y: auto; }
.gen-card .gc-body .line { display: block; }
.gen-card .gc-body .line b { color: #d96a00; }
.gc-foot { display: flex; gap: 8px; margin-top: 10px; }
.gc-btn {
flex: 1; border: 1px solid #ff7a1a; background: rgba(255,122,26,.06); color: #d96a00;
border-radius: 8px; padding: 7px 0; font-size: 12px; cursor: pointer; transition: all .2s; font-weight: 500;
}
.gc-btn:hover { background: #ff7a1a; color: #fff; }
.gc-btn.ghost { border-color: #e5e6eb; background: #fff; color: #4e5969; }
.gc-btn.ghost:hover { border-color: #ff7a1a; color: #d96a00; background: #fff; }
.tips-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.tip-pill { font-size: 11px; background: rgba(255,178,0,.08); color: #d97706; border-radius: 12px; padding: 3px 10px; }
.warn-pill { background: rgba(245,158,11,.1); color: #b45309; }
.ok-pill { background: rgba(34,197,94,.1); color: #15803d; }
.typing { display: inline-flex; gap: 4px; padding: 4px 2px; }
.typing span { width: 6px; height: 6px; background: #94a3b8; border-radius: 50%; animation: blink 1.2s infinite; }
.typing span:nth-child(2) { animation-delay: .2s; }
.typing span:nth-child(3) { animation-delay: .4s; }
@keyframes blink { 0%, 80%, 100% { opacity: .3; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
.ai-input-area { border-top: 1px solid #f2f3f5; padding: 12px 14px; flex-shrink: 0; background: #fff; }
.input-wrap { border: 1px solid #e5e6eb; border-radius: 12px; padding: 8px 8px 8px 12px; transition: border-color .2s, box-shadow .2s; }
.input-wrap:focus-within { border-color: #ff7a1a; box-shadow: 0 0 0 3px rgba(255,122,26,.1); }
.input-wrap textarea {
width: 100%; border: none; outline: none; resize: none; font-size: 13px; line-height: 1.6;
max-height: 90px; font-family: inherit; background: transparent;
}
.input-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 6px; }
.input-tools { display: flex; gap: 4px; }
.tool-btn {
width: 28px; height: 28px; border: none; background: transparent; border-radius: 6px;
cursor: pointer; color: #86909c; font-size: 14px; transition: all .2s;
}
.tool-btn:hover { background: #f2f3f5; color: #d96a00; }
.send-btn {
background: linear-gradient(135deg, #ff7a1a, #ff6600); border: none; color: #fff;
width: 60px; height: 30px; border-radius: 8px; font-size: 13px; cursor: pointer;
transition: opacity .2s; font-weight: 500;
}
.send-btn:hover { opacity: .88; }
.input-hint { text-align: center; font-size: 11px; color: #c9cdd4; margin-top: 8px; }
.ai-float {
position: absolute; right: 0; top: 50%; transform: translateY(-50%);
width: 36px; height: 92px; background: linear-gradient(135deg, #ff7a1a, #ffb300);
border-radius: 10px 0 0 10px; color: #fff; border: none; cursor: pointer; font-size: 13px;
writing-mode: vertical-lr; letter-spacing: 3px; display: none; z-index: 20;
box-shadow: -4px 0 12px rgba(255,122,26,.25);
}
.ai-panel.collapsed .ai-float { display: block; }
.toast {
position: fixed; top: 68px; left: 50%; transform: translateX(-50%) translateY(-12px);
background: #1f2329; color: #fff; padding: 9px 20px; border-radius: 8px; font-size: 13px;
opacity: 0; pointer-events: none; transition: all .3s; z-index: 999;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-thumb { background: #d5d9e0; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #b8bec9; }
::-webkit-scrollbar-track { background: transparent; }
/* ============ 自主发品 Agent 卡片 ============ */
.agent-card {
border: 1px solid rgba(255, 122, 26, 0.45); border-radius: 12px; overflow: hidden;
background: linear-gradient(180deg, #fffaf3, #fff); margin-top: 4px;
}
.ag-head { background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff; padding: 9px 12px; font-size: 13px; font-weight: 600; }
.ag-ctx { padding: 8px 12px; font-size: 12px; color: #d96a00; background: #fff7ed; border-bottom: 1px dashed rgba(255,122,26,.25); }
.ag-steps { padding: 4px 0; }
.ag-step { display: flex; align-items: center; gap: 8px; padding: 7px 12px; font-size: 12.5px; color: #4e5969; border-bottom: 1px solid #f7f3ee; }
.ag-step:last-child { border-bottom: none; }
.ag-step .ag-ico { width: 18px; text-align: center; font-size: 13px; flex-shrink: 0; }
.ag-step .ag-lbl { flex: 1; }
.ag-step .ag-note { font-size: 11px; color: #86909c; }
.ag-step.todo .ag-note { color: #c0c4cc; }
.ag-step.doing { color: #d96a00; background: #fff8ef; }
.ag-step.doing .ag-ico { animation: agspin .8s linear infinite; display: inline-block; }
.ag-step.doing .ag-note { color: #d96a00; }
.ag-step.done .ag-lbl { color: #1f2329; }
.ag-step.done .ag-ico { color: #22c55e; }
@keyframes agspin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
.ag-foot { padding: 8px 12px; font-size: 11px; color: #86909c; background: #fafafa; border-top: 1px solid #f0f0f0; }
.ag-chip { display: inline-block; background: #fff; border: 1px solid rgba(255,122,26,.4); color: #ff7a1a; border-radius: 10px; padding: 1px 7px; font-size: 11px; margin: 0 3px 3px 0; }
/* ============ 串行确认栏 ============ */
.ag-confirm-bar {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
margin-top: 10px; padding: 9px 12px;
background: linear-gradient(135deg, #fff8ef, #fffaf3);
border: 1px solid rgba(255,122,26,.3); border-radius: 8px;
}
.ag-cf-btn {
background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff; border: none;
padding: 7px 18px; border-radius: 6px; font-size: 12.5px; cursor: pointer;
font-weight: 600; font-family: inherit; transition: all .2s; white-space: nowrap;
}
.ag-cf-btn:hover { transform: translateY(-1px); box-shadow: 0 3px 10px rgba(255,122,26,.35); }
.ag-regen-btn { padding: 7px 14px; font-size: 12px; border-radius: 6px; border: 1px solid #e5e6eb; background: #fff; color: #4e5969; cursor: pointer; font-family: inherit; transition: all .15s; white-space: nowrap; }
.ag-regen-btn:hover { border-color: #ff7a1a; color: #d96a00; }
.ag-cd { font-size: 11px; color: #d96a00; margin-left: auto; white-space: nowrap; }
.ag-cd.manual { color: #86909c; }
.ag-mode-row {
display: flex; align-items: center; gap: 6px;
padding: 7px 12px; background: #fff7ed; border-bottom: 1px solid rgba(255,122,26,.12);
}
.ag-mode-label { font-size: 11px; color: #86909c; }
.ag-mt-btn {
border: 1px solid rgba(255,122,26,.3); background: #fff; color: #d96a00;
padding: 3px 10px; border-radius: 10px; font-size: 11px; cursor: pointer;
font-family: inherit; transition: all .15s;
}
.ag-mt-btn.active { background: #ff7a1a; color: #fff; border-color: #ff7a1a; }
.serial-card .gc-foot { display: none; }
/* ============ 一键发品 ============ */
.btn-magic {
background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff; border: none;
padding: 10px 26px; border-radius: 10px; font-size: 15px; cursor: pointer; font-weight: 600;
box-shadow: 0 4px 14px rgba(255,122,26,.35); transition: all .2s; font-family: inherit;
}
.btn-magic:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(255,122,26,.45); }
.btn-ghost-w {
border: 1px solid #e5e6eb; background: #fff; color: #4e5969; padding: 9px 22px;
border-radius: 8px; font-size: 14px; cursor: pointer; transition: all .2s; font-family: inherit;
}
.btn-ghost-w:hover { border-color: #ff7a1a; color: #d96a00; }
.modal-mask {
position: fixed; inset: 0; background: rgba(15,23,42,.45); backdrop-filter: blur(3px);
display: none; align-items: center; justify-content: center; z-index: 1000;
}
.modal-mask.show { display: flex; }
.modal {
width: 530px; max-height: 84vh; overflow-y: auto; background: #fff; border-radius: 16px;
padding: 24px; animation: pop .25s ease;
}
@keyframes pop { from { opacity: 0; transform: scale(.95) translateY(10px); } to { opacity: 1; transform: none; } }
.modal h3 { font-size: 17px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
.modal .sub { font-size: 12px; color: #86909c; margin-top: 4px; }
.learn-hint {
margin-top: 14px; background: linear-gradient(135deg, rgba(255,178,0,.1), rgba(255,122,26,.08));
border: 1px solid #ffe3c2; border-radius: 10px; padding: 12px; font-size: 12px; line-height: 1.8; color: #4e5969;
}
.learn-hint b { color: #d96a00; }
.stat-row { display: flex; gap: 10px; margin-top: 14px; }
.stat-box { flex: 1; background: #f7f8fa; border-radius: 10px; padding: 10px; text-align: center; }
.stat-box .v { font-size: 18px; font-weight: 700; color: #d96a00; }
.stat-box .v small { font-size: 12px; font-weight: 400; color: #86909c; }
.stat-box .k { font-size: 11px; color: #86909c; margin-top: 2px; }
.level-wrap { margin-top: 14px; }
.level-bar { height: 6px; background: #f2f3f5; border-radius: 3px; overflow: hidden; margin-top: 6px; }
.level-bar i { display: block; height: 100%; background: linear-gradient(90deg, #ffa200, #ff6600); border-radius: 3px; transition: width .6s; }
.level-text { font-size: 12px; color: #4e5969; display: flex; justify-content: space-between; }
.conf-row { display: flex; gap: 12px; margin-top: 16px; }
.step-list { margin-top: 18px; display: flex; flex-direction: column; gap: 10px; }
.step-item {
display: flex; align-items: center; gap: 10px; font-size: 13px; color: #86909c;
padding: 10px 12px; border: 1px solid #f2f3f5; border-radius: 10px; background: #fafbfc; transition: all .3s;
}
.step-item .s-icon {
width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font-size: 12px; background: #f2f3f5; color: #86909c; flex-shrink: 0;
}
.step-item.doing { border-color: #ffd9ae; background: rgba(255,178,0,.06); color: #1f2329; }
.step-item.doing .s-icon { background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.step-item.done { color: #1f2329; }
.step-item.done .s-icon { background: #22c55e; color: #fff; }
.step-item .s-note { margin-left: auto; font-size: 11px; color: #c9cdd4; }
.step-item.done .s-note { color: #15803d; }
.modal-foot { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; }
.done-view { text-align: center; padding: 8px 0; }
.done-view .big { font-size: 42px; }
.done-view h4 { font-size: 16px; margin: 8px 0 4px; }
.done-view .dsub { font-size: 12px; color: #86909c; }
.oc-wide {
width: 100%; border: none; background: linear-gradient(135deg, #ffa200, #ff6600); color: #fff;
border-radius: 10px; padding: 10px; font-size: 13px; font-weight: 600; cursor: pointer;
margin-bottom: 10px; box-shadow: 0 3px 10px rgba(255,122,26,.25); transition: all .2s; font-family: inherit;
}
.oc-wide:hover { transform: translateY(-1px); box-shadow: 0 5px 14px rgba(255,122,26,.35); }
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="topbar">
<div class="logo"><div class="icon">✈</div> 旅智工作台</div>
<div class="breadcrumb">商品管理 / <b>发布新商品</b></div>
<div class="right">
<span>草稿自动保存于 1 分钟前</span>
<div class="avatar-sm">李</div>
</div>
</div>
<div class="main">
<!-- 左侧:商品发布表单 -->
<div class="form-area" id="formArea">
<div class="page-head">
<div>
<h1>发布新商品</h1>
<div class="sub">完善商品信息,或用「一键发品」30 秒搞定 · 右侧 AI 助手随时待命 ↗</div>
</div>
<div style="display:flex; gap:10px">
<button class="btn-ghost-w" onclick="toast('已提交审核(演示)')">提交审核</button>
<button class="btn-magic" onclick="openOC()">⚡ 一键发品</button>
</div>
</div>
<!-- 1. 商品标题 -->
<div class="card" id="card-title">
<h2><span class="dot"></span>商品标题<button class="ai-inline-btn" onclick="formAI('title')" title="AI 生成 3 个高转化标题">✨ AI 生成标题</button><span class="badge-step">模块 1/6</span></h2>
<div class="form-row">
<div class="form-item" style="flex:2">
<label><span class="req">*</span>商品标题</label>
<input id="f-title" type="text" maxlength="40" placeholder="例:云南昆明+大理+丽江6日5晚纯玩跟团游">
<div class="counter"><span id="titleCount">0</span>/40 字</div>
</div>
<div class="form-item">
<label>目的地</label>
<select id="f-dest">
<option>云南(昆明/大理/丽江)</option>
<option>四川(成都/九寨沟)</option>
<option>海南(三亚)</option>
<option>新疆(乌鲁木齐/喀纳斯)</option>
<option>西藏(拉萨/林芝)</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-item">
<label>行程天数</label>
<select id="f-days">
<option>6天5晚</option><option>3天2晚</option><option>5天4晚</option>
<option>7天6晚</option><option>8天7晚</option>
</select>
</div>
<div class="form-item">
<label>商品类型</label>
<select id="f-type">
<option>跟团游</option><option>自由行</option><option>定制游</option>
<option>半自助游</option><option>当地玩乐</option>
</select>
</div>
<div class="form-item" style="flex:2">
<label>适合人群</label>
<div class="chips" id="f-tags">
<span class="chip active">亲子家庭</span>
<span class="chip">蜜月情侣</span>
<span class="chip">银发无忧</span>
<span class="chip">摄影爱好</span>
<span class="chip">小众深度</span>
</div>
</div>
</div>
</div>
<!-- 2. 详细介绍 -->
<div class="card" id="card-intro">
<h2><span class="dot"></span>详细介绍<button class="ai-inline-btn" onclick="formAI('intro')" title="AI 生成四段式卖点摘要">✨ AI 生成卖点</button><span class="badge-step">模块 2/6</span></h2>
<div class="form-item">
<label><span class="req">*</span>详细介绍(列表页展示的卖点摘要)</label>
<textarea id="f-intro" rows="5" placeholder="突出行程亮点、住宿标准、服务承诺等核心卖点…"></textarea>
<div class="hint">建议 150-300 字,聚焦用户最关心的 3-4 个卖点</div>
</div>
</div>
<!-- 3. 商品详情 -->
<div class="card" id="card-detail">
<h2><span class="dot"></span>商品详情<button class="ai-inline-btn" onclick="formAI('detail')" title="AI 生成结构化图文详情">✨ AI 生成详情</button><span class="badge-step">模块 3/6</span></h2>
<div class="form-item">
<label><span class="req">*</span>详情页图文内容</label>
<div class="rich-editor">
<div class="rich-toolbar">
<button title="加粗">B</button>
<button title="斜体">I</button>
<button title="标题">H</button>
<div class="sep"></div>
<button title="插入图片">🖼️</button>
<button title="插入视频">🎬</button>
<div class="sep"></div>
<button title="行程模块">🗺️</button>
<button title="费用说明">¥</button>
</div>
<div class="rich-content" id="f-detail" contenteditable="true" data-placeholder="支持图文混排,AI 可生成结构化详情内容…"></div>
</div>
</div>
</div>
<!-- 4. 商品图片 -->
<div class="card" id="card-img">
<h2><span class="dot"></span>商品图片<button class="ai-inline-btn" onclick="formAI('image')" title="AI 推荐拍摄脚本与配图清单">✨ AI 配图建议</button><span class="badge-step">模块 4/6</span></h2>
<div class="form-item">
<label><span class="req">*</span>主图与详情图(首张为主图,建议 800×600 以上)</label>
<div class="img-grid" id="imgGrid"></div>
<div class="hint" style="margin-top:10px">AI 可根据行程内容推荐拍摄脚本与配图清单</div>
</div>
</div>
<!-- 5. 行程编辑 -->
<div class="card" id="card-itinerary">
<h2><span class="dot"></span>行程编辑<button class="ai-inline-btn" onclick="formAI('itinerary')" title="AI 按天数生成逐日行程">✨ AI 规划行程</button><span class="badge-step">模块 5/6</span></h2>
<div id="itineraryList"></div>
<button class="add-day" onclick="addDay()">+ 添加一天行程</button>
</div>
<!-- 6. 价格库存 -->
<div class="card" id="card-price">
<h2><span class="dot"></span>价格库存<button class="ai-inline-btn" onclick="formAI('price')" title="AI 生成团期排期与定价方案">✨ AI 排期定价</button><span class="badge-step">模块 6/6</span></h2>
<table class="price-table">
<thead>
<tr>
<th style="width:150px">出发日期</th>
<th>成人价(元)</th>
<th>儿童价(元)</th>
<th style="width:90px">库存</th>
<th style="width:80px">状态</th>
<th style="width:40px"></th>
</tr>
</thead>
<tbody id="priceBody"></tbody>
</table>
<button class="add-day" style="margin-top:10px" onclick="addPriceRow()">+ 添加团期</button>
</div>
</div>
<!-- 右侧:AI 助手 -->
<div class="ai-panel" id="aiPanel">
<button class="ai-float" onclick="togglePanel()">AI 助手</button>
<div class="ai-header">
<div class="ai-avatar">🤖</div>
<div class="info">
<div class="name">旅智小助 <span class="badge">商品发布专精</span></div>
<div class="status">在线 · 已读取当前页面表单内容</div>
</div>
<div class="actions">
<button class="icon-btn" title="收起面板" onclick="togglePanel()">»</button>
</div>
</div>
<div class="ai-quick">
<div class="title">⚡ 快捷生成(对应左侧 6 大模块)</div>
<button class="oc-wide" onclick="openOC()">⚡ 一键发品 · 30 秒完成 6 大模块</button>
<div class="quick-grid">
<button class="quick-btn" onclick="quickAction('title')">
<span class="qicon q1">📝</span><span class="qtext">商品标题</span>
</button>
<button class="quick-btn" onclick="quickAction('intro')">
<span class="qicon q2">💎</span><span class="qtext">详细介绍</span>
</button>
<button class="quick-btn" onclick="quickAction('detail')">
<span class="qicon q7">📄</span><span class="qtext">商品详情</span>
</button>
<button class="quick-btn" onclick="quickAction('image')">
<span class="qicon q5">🖼️</span><span class="qtext">配图建议</span>
</button>
<button class="quick-btn" onclick="quickAction('itinerary')">
<span class="qicon q3">🗺️</span><span class="qtext">行程规划</span>
</button>
<button class="quick-btn" onclick="quickAction('price')">
<span class="qicon q4">💰</span><span class="qtext">价格库存</span>
</button>
<button class="quick-btn" onclick="quickAction('seo')">
<span class="qicon q6">🔍</span><span class="qtext">SEO 词</span>
</button>
<button class="quick-btn" onclick="quickAction('check')">
<span class="qicon q6">🛡️</span><span class="qtext">合规检查</span>
</button>
</div>
</div>
<div class="ai-messages" id="msgList"></div>
<div class="ai-input-area">
<div class="input-wrap">
<textarea id="chatInput" rows="1" placeholder="例如:帮我优化标题和详情 / 帮我发个海南亲子6天自由行" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea>
<div class="input-foot">
<div class="input-tools">
<button class="tool-btn" title="参考示例">💡</button>
<button class="tool-btn" title="上传图片">🖼️</button>
</div>
<button class="send-btn" onclick="sendMessage()">发送</button>
</div>
</div>
<div class="input-hint">Enter 发送 · Shift+Enter 换行 · 内容由 AI 生成,请核对后发布</div>
</div>
</div>
</div>
<!-- 一键发品弹窗 -->
<div class="modal-mask" id="ocMask" onclick="if(event.target===this)closeOC()">
<div class="modal">
<!-- 视图1:配置 -->
<div id="ocConfig">
<h3>⚡ 一键发品</h3>
<div class="sub">选好基础信息,AI 自动完成 6 大模块填写,30 秒搞定发品</div>
<div class="learn-hint" id="ocLearn" style="display:none"></div>
<div class="stat-row">
<div class="stat-box"><div class="v" id="ocCount">0</div><div class="k">累计发品(次)</div></div>
<div class="stat-box"><div class="v" id="ocPrefN">0</div><div class="k">已沉淀偏好(条)</div></div>
<div class="stat-box"><div class="v">1<small> 分钟</small></div><div class="k">发品耗时(传统约 30 分钟)</div></div>
</div>
<div class="level-wrap">
<div class="level-text"><span id="ocLevel"></span><span id="ocLevelNext"></span></div>
<div class="level-bar"><i id="ocBar" style="width:0%"></i></div>
</div>
<div class="conf-row">
<div class="form-item">
<label>目的地</label>
<select id="oc-dest">
<option>云南(昆明/大理/丽江)</option>
<option>四川(成都/九寨沟)</option>
<option>海南(三亚)</option>
<option>新疆(乌鲁木齐/喀纳斯)</option>
<option>西藏(拉萨/林芝)</option>
</select>
</div>
<div class="form-item">
<label>天数</label>
<select id="oc-days">
<option>6天5晚</option><option>3天2晚</option><option>5天4晚</option>
<option>7天6晚</option><option>8天7晚</option>
</select>
</div>
<div class="form-item">
<label>类型</label>
<select id="oc-type">
<option>跟团游</option><option>自由行</option><option>定制游</option>
<option>半自助游</option><option>当地玩乐</option>
</select>
</div>
</div>
<div class="form-item">
<label>适合人群</label>
<div class="chips" id="oc-tags">
<span class="chip active">亲子家庭</span>
<span class="chip">蜜月情侣</span>
<span class="chip">银发无忧</span>
<span class="chip">摄影爱好</span>
<span class="chip">小众深度</span>
</div>
</div>
<div class="modal-foot">
<button class="btn-ghost-w" onclick="closeOC()">取消</button>
<button class="btn-magic" onclick="startOC()">⚡ 开始生成</button>
</div>
</div>
<!-- 视图2:生成进度 -->
<div id="ocProgress" style="display:none">
<h3>🤖 AI 正在发品…</h3>
<div class="sub">按您的偏好逐个模块生成,完成后可随时接手微调</div>
<div class="step-list" id="ocSteps"></div>
</div>
<!-- 视图3:完成 -->
<div id="ocDone" style="display:none">
<div class="done-view">
<div class="big">🎉</div>
<h4>发品完成!</h4>
<div class="dsub">6 大模块已全部生成,请核对后提交审核</div>
</div>
<div class="learn-hint" id="ocNewPref"></div>
<div class="modal-foot">
<button class="btn-ghost-w" onclick="closeOC()">返回核对</button>
<button class="btn-magic" onclick="closeOC();toast('已提交审核(演示)')">提交审核</button>
</div>
</div>
</div>
</div>
<div class="toast" id="toast">✓ 已填入</div>
<script>
/* ================= 状态 ================= */
const $ = id => document.getElementById(id);
const msgList = $('msgList');
let ctx = { dest: '云南', days: '6天5晚', type: '跟团游', theme: '亲子家庭' };
let itineraryDays = [];
let priceRows = [];
let images = [];
const generated = {}; // 每类动作最新生成结果
const esc = s => s.replace(/</g, '&lt;');
let silentBatch = false; // 一键发品批量模式:静默填入,不逐条弹提示
/* ================= 工具 ================= */
function toast(t) {
if (silentBatch) return;
const el = $('toast');
el.textContent = '✓ ' + t;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), 1800);
}
function scrollToBottom() {
requestAnimationFrame(() => { msgList.scrollTop = msgList.scrollHeight; });
}
function readForm() {
ctx.dest = $('f-dest').value.replace(/(.*)/, '') || ctx.dest;
ctx.days = $('f-days').value || ctx.days;
ctx.type = $('f-type').value || ctx.type;
const tag = document.querySelector('#f-tags .chip.active');
if (tag) ctx.theme = tag.textContent;
}
function flashCard(cardId) {
if (silentBatch) return;
const el = $(cardId);
el.classList.add('section-highlight');
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => el.classList.remove('section-highlight'), 1800);
}
/* ================= 消息渲染 ================= */
function addMsg(role, html) {
const div = document.createElement('div');
div.className = 'msg ' + role;
div.innerHTML = role === 'ai'
? '<div class="m-avatar">🤖</div><div class="bubble">' + html + '</div>'
: '<div class="m-avatar">李</div><div class="bubble">' + html + '</div>';
msgList.appendChild(div);
scrollToBottom();
return div;
}
function typing() { return addMsg('ai', '<div class="typing"><span></span><span></span><span></span></div>'); }
function replaceTyping(div, html) { div.querySelector('.bubble').innerHTML = html; scrollToBottom(); }
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
/* ================= 表单渲染:图片 ================= */
const imgPalette = [
{ g: 'linear-gradient(135deg,#ffb300,#ff7a1a)', em: '🚤' },
{ g: 'linear-gradient(135deg,#f59e0b,#ef4444)', em: '🏮' },
{ g: 'linear-gradient(135deg,#8b5cf6,#ec4899)', em: '🏔️' },
{ g: 'linear-gradient(135deg,#22c55e,#ffb300)', em: '🏨' },
{ g: 'linear-gradient(135deg,#f97316,#facc15)', em: '🍲' },
{ g: 'linear-gradient(135deg,#6366f1,#ffb300)', em: '🌅' },
];
function renderImages() {
const grid = $('imgGrid');
let html = images.map((im, i) => {
const p = imgPalette[i % imgPalette.length];
return `<div class="img-tile" style="background:${p.g}">
<span class="tag">${i === 0 ? '主图' : '详情图 ' + i}</span>
${im.ai ? '<span class="ai-tag">AI 建议</span>' : ''}
<span class="em">${p.em}</span><span>${im.label}</span>
<button class="del" onclick="removeImage(${i})">✕</button>
</div>`;
}).join('');
html += `<div class="img-upload" onclick="uploadImg()"><span class="plus">+</span>上传图片</div>`;
grid.innerHTML = html;
}
function removeImage(i) { images.splice(i, 1); renderImages(); }
function uploadImg() {
images.push({ label: '新上传图片', ai: false });
renderImages();
toast('已添加图片占位(演示)');
}
/* ================= 表单渲染:行程 ================= */
function renderItinerary() {
const list = $('itineraryList');
if (!itineraryDays.length) {
list.innerHTML = '<div class="empty-tip">暂无行程,点击下方按钮添加,或让 AI 一键生成 ↗</div>';
return;
}
list.innerHTML = itineraryDays.map((d, i) => `
<div class="day-card">
<div class="day-head">
<span class="dnum">D${i + 1}</span>
<input value="${esc(d.title)}" placeholder="当日主题,如:抵达昆明·入住酒店" oninput="itineraryDays[${i}].title=this.value">
<button class="del-day" title="删除当天" onclick="removeDay(${i})">✕</button>
</div>
<div class="day-body">
<textarea placeholder="当日安排描述…" oninput="itineraryDays[${i}].desc=this.value">${esc(d.desc)}</textarea>
</div>
</div>`).join('');
}
function addDay() { itineraryDays.push({ title: '', desc: '' }); renderItinerary(); }
function removeDay(i) { itineraryDays.splice(i, 1); renderItinerary(); }
/* ================= 表单渲染:价格库存 ================= */
function renderPrice() {
const body = $('priceBody');
if (!priceRows.length) {
body.innerHTML = '<tr><td colspan="6"><div class="empty-tip">暂无团期,点击下方按钮添加,或让 AI 生成排期方案 ↗</div></td></tr>';
return;
}
body.innerHTML = priceRows.map((r, i) => `
<tr>
<td><input value="${r.date}" oninput="priceRows[${i}].date=this.value"></td>
<td><input type="number" value="${r.adult}" oninput="priceRows[${i}].adult=this.value"></td>
<td><input type="number" value="${r.child}" oninput="priceRows[${i}].child=this.value"></td>
<td><input type="number" value="${r.stock}" class="${r.stock < 6 ? 'stock-low' : ''}" oninput="priceRows[${i}].stock=+this.value;renderPrice()"></td>
<td><span class="status-pill ${r.on ? 'status-on' : 'status-off'}" style="cursor:pointer" onclick="priceRows[${i}].on=!priceRows[${i}].on;renderPrice()">${r.on ? '在售' : '停售'}</span></td>
<td><button class="del-row" onclick="removePriceRow(${i})">✕</button></td>
</tr>`).join('');
}
function addPriceRow() {
priceRows.push({ date: '2026-10-01', adult: 3299, child: 2299, stock: 20, on: true });
renderPrice();
}
function removePriceRow(i) { priceRows.splice(i, 1); renderPrice(); }
/* ================= 一键填入 ================= */
const applyMap = {
title() {
const opts = generated.title.data;
const idx = generated.title.used || 0;
const t = opts[idx % opts.length];
generated.title.used = idx + 1;
$('f-title').value = t;
$('titleCount').textContent = t.length;
$('f-title').classList.add('field-highlight');
setTimeout(() => $('f-title').classList.remove('field-highlight'), 1600);
flashCard('card-title');
toast('已填入商品标题(方案 ' + ((idx % opts.length) + 1) + ')');
},
intro() {
$('f-intro').value = generated.intro.data;
$('f-intro').classList.add('field-highlight');
setTimeout(() => $('f-intro').classList.remove('field-highlight'), 1600);
flashCard('card-intro');
toast('已填入详细介绍');
},
detail() {
$('f-detail').innerHTML = generated.detail.data;
flashCard('card-detail');
toast('已生成商品详情图文结构');
},
image() {
generated.image.data.forEach(im => images.push({ label: im.label, ai: true }));
renderImages();
flashCard('card-img');
toast('已按 AI 清单添加 ' + generated.image.data.length + ' 张配图');
},
itinerary() {
itineraryDays = generated.itinerary.data.map(d => ({ ...d }));
renderItinerary();
flashCard('card-itinerary');
toast('已填入 ' + itineraryDays.length + ' 天行程');
},
price() {
priceRows = generated.price.data.map(r => ({ ...r }));
renderPrice();
flashCard('card-price');
toast('已生成 ' + priceRows.length + ' 个团期');
}
};
function applyGen(type) { if (generated[type]) applyMap[type](); }
function regen(type) { quickAction(type, true); }
/* ================= 快捷动作数据 ================= */
const genData = {
title() {
readForm();
const t = [
`【纯玩无购物】${ctx.dest}${ctx.days}精品${ctx.type}·5A景区全含·${ctx.theme}首选`,
`${ctx.dest}${ctx.days}深度${ctx.type}|一价全含0自费·住${ctx.dest === '云南' ? '海景' : '景区'}酒店`,
`【${ctx.theme}必选】${ctx.dest}${ctx.days}${ctx.type}·随走随停·金牌向导·赠${ctx.dest === '云南' ? '洱海游船' : '旅拍'}体验`
];
return {
pre: `结合 <span class="hl">${ctx.dest}·${ctx.days}·${ctx.type}·${ctx.theme}</span> 生成 3 个高点击标题(可多次点击填入切换方案):`,
card: `<div class="gen-card">
<div class="gc-head">✨ 标题方案(三选一)</div>
<div class="gc-body">${t.map((x, i) => `<span class="line"><b>${i + 1}.</b> ${x}</span>`).join('')}</div>
<div class="gc-foot">
<button class="gc-btn" onclick="applyGen('title')">⚡ 一键填入「商品标题」</button>
<button class="gc-btn ghost" onclick="regen('title')">🔄 换一版</button>
</div>
<div class="tips-row">
<span class="tip-pill ok-pill">含目的地+天数+类型,利于搜索</span>
<span class="tip-pill">「无购物」为平台高转化词</span>
</div>
</div>`,
data: t
};
},
intro() {
readForm();
const d = `【${ctx.dest}${ctx.days}${ctx.type}·${ctx.theme}优选】全程 0 购物 0 自费,景区门票一价全含,金牌持证导游全程讲解。\n精选 ${ctx.days === '3天2晚' ? '2' : '4'} 晚高分酒店,${ctx.dest === '云南' ? '大理海景房 + 丽江古城客栈' : '近景区步行可达'},住的舒心才是真度假。\n特色体验:${ctx.dest === '云南' ? '白族扎染 + 洱海游船 + 野生菌火锅' : '深度环线,不赶路'},孩子玩得开心、大人拍得尽兴。\n服务承诺:行程透明无隐形消费,24h 专属管家,未成团提前 3 天全额退款。`;
return {
pre: `围绕「${ctx.theme}」人群痛点,按「亮点—住宿—体验—承诺」四段式生成详细介绍:`,
card: `<div class="gen-card">
<div class="gc-head">✨ 详细介绍(列表页卖点摘要)</div>
<div class="gc-body">${d.split('\n').map(l => `<span class="line">${l}</span>`).join('')}</div>
<div class="gc-foot">
<button class="gc-btn" onclick="applyGen('intro')">⚡ 一键填入「详细介绍」</button>
<button class="gc-btn ghost" onclick="regen('intro')">🔄 换一版</button>
</div>
<div class="tips-row">
<span class="tip-pill ok-pill">约 200 字,符合最佳展示长度</span>
<span class="tip-pill">含服务承诺,降低决策门槛</span>
</div>
</div>`,
data: d
};
},
detail() {
readForm();
const html = `<h3>🌟 行程亮点</h3>
<ul>
<li>全程 <b>0 购物 0 自费</b>,行程透明,把时间还给风景</li>
<li>${ctx.dest}经典 IP 全含:${ctx.dest === '云南' ? '石林、洱海、玉龙雪山、丽江古城' : '核心景区深度游览'}</li>
<li>${ctx.theme}专属节奏:每日行车 ≤ 3.5 小时,不赶路不早起(可睡到自然醒)</li>
</ul>
<h3>🏨 住宿安排</h3>
<ul>
<li>${ctx.dest === '云南' ? '大理 1 晚海景房 + 丽江 2 晚古城客栈' : '全程高分舒适型酒店'}</li>
<li>提前公布酒店名单,拒绝到店换酒店</li>
</ul>
<h3>🍜 舌尖体验</h3>
<ul>
<li>${ctx.dest === '云南' ? '野生菌火锅 + 白族风味餐 + 过桥米线' : '当地特色风味餐 ' + 3 + ' 次'}</li>
</ul>
<h3>🛡️ 服务保障</h3>
<ul>
<li>持证金牌导游 + 24h 专属管家</li>
<li>未成团提前 3 天全额退款,出行无忧</li>
</ul>
<div class="img-slot">🖼️ 此处建议插入:行程地图 / 实拍酒店 / 餐饮实拍(见「配图建议」)</div>`;
return {
pre: `已生成结构化商品详情(亮点/住宿/餐饮/保障 + 配图占位),可直接在编辑器中继续润色:`,
card: `<div class="gen-card">
<div class="gc-head">✨ 商品详情(图文结构)</div>
<div class="gc-body"><span class="line"><b>行程亮点</b> · 0购物0自费 / 经典IP全含 / ${ctx.theme}节奏</span><span class="line"><b>住宿安排</b> · ${ctx.dest === '云南' ? '海景房+古城客栈' : '高分酒店'}</span><span class="line"><b>舌尖体验</b> · ${ctx.dest === '云南' ? '野生菌火锅+风味餐' : '特色餐 3 次'}</span><span class="line"><b>服务保障</b> · 金牌导游 / 24h管家 / 全额退款承诺</span><span class="line"><b>配图占位</b> · 行程地图 / 酒店实拍 / 餐饮实拍</span></div>
<div class="gc-foot">
<button class="gc-btn" onclick="applyGen('detail')">⚡ 写入「商品详情」编辑器</button>
<button class="gc-btn ghost" onclick="regen('detail')">🔄 换一版</button>
</div>
</div>`,
data: html
};
},
image() {
readForm();
const list = [
{ label: `${ctx.dest}全景主图(横版大图)`, use: '主图' },
{ label: '酒店实拍·房间内饰', use: '详情' },
{ label: '核心景区打卡机位', use: '详情' },
{ label: '特色餐食实拍', use: '详情' },
{ label: `${ctx.theme}互动瞬间抓拍`, use: '详情' },
{ label: '行程地图示意', use: '详情' }
];
return {
pre: `根据行程内容生成 <b>6 张配图拍摄脚本</b>(含用途与构图建议):`,
card: `<div class="gen-card">
<div class="gc-head">✨ 配图清单(转化率优选顺序)</div>
<div class="gc-body">${list.map((im, i) => `<span class="line"><b>${i + 1}.</b> ${im.label} <span style="color:#86909c;font-size:11px">[${im.use}]</span></span>`).join('')}</div>
<div class="gc-foot">
<button class="gc-btn" onclick="applyGen('image')">⚡ 按清单添加到「商品图片」</button>
<button class="gc-btn ghost" onclick="regen('image')">🔄 换一版</button>
</div>
<div class="tips-row">
<span class="tip-pill ok-pill">主图含地标,点击率 +18%</span>
<span class="tip-pill">实拍图 > 精修图,差评更少</span>
</div>
</div>`,
data: list.map(x => ({ label: x.use === '主图' ? x.label : x.label.replace(/(.*)/, '') }))
};
},
itinerary() {
readForm();
const isYn = ctx.dest === '云南';
const days = [
{ title: isYn ? '抵达昆明·专车接站' : `抵达${ctx.dest}·专车接站`, desc: '落地即接,24h 接站服务,入住酒店后自由活动,推荐逛逛翠湖公园。' },
{ title: isYn ? '石林风景区 → 楚雄' : '核心景区深度游', desc: '上午游览 5A 石林,午后赴楚雄,晚观彝族歌舞表演。' },
{ title: isYn ? '大理古城 → 洱海游船 → 双廊' : '慢节奏主题日', desc: '上午漫步大理古城,午后洱海游船,双廊海边下午茶,看日落。' },
{ title: isYn ? '丽江古城 → 玉龙雪山 → 蓝月谷' : '自由活动日', desc: '索道登玉龙雪山4506m,蓝月谷拍照,晚逛丽江四方街。' },
{ title: isYn ? '束河古镇 → 返回昆明' : '自由活动·休整', desc: '睡到自然醒,逛束河古镇,午后大巴返回昆明。' },
{ title: '返程日·专车送站', desc: '根据航班时间安排送站,行程圆满结束。' }
].slice(0, parseInt(ctx.days) || 6);
return {
pre: `按 <span class="hl">${ctx.days}·${ctx.type}·${ctx.theme}</span> 节奏规划每日行程,张弛有度:`,
card: `<div class="gen-card">
<div class="gc-head">✨ 行程框架(每日含主题与安排)</div>
<div class="gc-body">${days.map((d, i) => `<span class="line"><b>D${i + 1}</b> ${d.title}</span>`).join('')}</div>
<div class="gc-foot">
<button class="gc-btn" onclick="applyGen('itinerary')">⚡ 填入「行程编辑」</button>
<button class="gc-btn ghost" onclick="regen('itinerary')">🔄 换一版</button>
</div>
<div class="tips-row">
<span class="tip-pill ok-pill">每日行车 ≤ 3.5h,适合${ctx.theme}</span>
<span class="tip-pill">D1/D6 只做接驳,差评率更低</span>
</div>
</div>`,
data: days
};
},
price() {
readForm();
const rows = [
{ date: '2026-09-12', adult: 3299, child: 2299, stock: 18, on: true },
{ date: '2026-09-26', adult: 3499, child: 2399, stock: 12, on: true },
{ date: '2026-10-01', adult: 4199, child: 2899, stock: 25, on: true },
{ date: '2026-10-03', adult: 4199, child: 2899, stock: 5, on: true },
{ date: '2026-10-17', adult: 3399, child: 2349, stock: 20, on: true }
];
return {
pre: `基于近 30 天 <span class="hl">${ctx.dest}·${ctx.days}·${ctx.type}</span> 成交数据,生成 5 个团期排期与定价(含国庆档):`,
card: `<div class="gen-card">
<div class="gc-head">✨ 价格库存方案</div>
<div class="gc-body"><span class="line">📊 市场区间:<b>¥2,899-¥4,199</b>(${ctx.theme}均价 ¥3,480)</span><span class="line">🎯 平季定价:<b>¥3,299</b>(低于均价 5%,点击率预估 +23%)</span><span class="line">📈 国庆档:<b>¥4,199</b>·库存充足(预计售罄率 85%)</span><span class="line">💡 策略:首月立减 ¥300 冲新品榜,稳定后回调</span></div>
<div class="gc-foot">
<button class="gc-btn" onclick="applyGen('price')">⚡ 填入「价格库存」表</button>
<button class="gc-btn ghost" onclick="regen('price')">🔄 换一版</button>
</div>
<div class="tips-row">
<span class="tip-pill ok-pill">低于均价 5%,排序更优</span>
<span class="tip-pill warn-pill">10-03 团期库存偏低,建议 +10</span>
</div>
</div>`,
data: rows
};
},
seo() {
readForm();
const kw = `${ctx.dest}旅游,${ctx.dest}${ctx.days},${ctx.type},${ctx.theme}旅游,${ctx.dest}纯玩无购物,${ctx.dest}自由行攻略`;
return {
pre: `提取 <b>6 个高流量关键词</b>,按搜索热度排序:`,
card: `<div class="gen-card">
<div class="gc-head">✨ SEO 关键词(月均搜索指数)</div>
<div class="gc-body">${kw.split(',').map((k, i) => `<span class="line"><b>${i + 1}.</b> ${k} <span style="color:#86909c;font-size:11px">热度 ${[8200, 6500, 5400, 3900, 2800, 1900][i]}</span></span>`).join('')}</div>
<div class="gc-foot">
<button class="gc-btn" onclick="toast('SEO 词已复制(演示)')">📋 复制关键词</button>
<button class="gc-btn ghost" onclick="regen('seo')">🔄 换一版</button>
</div>
</div>`,
data: kw
};
},
check() {
return {
pre: `已对 6 大模块进行 <b>广告法 + 平台规则</b> 双重合规扫描:`,
card: `<div class="gen-card">
<div class="gc-head">🛡️ 合规检查报告</div>
<div class="gc-body">
<span class="line">✅ 标题:无「最」「第一」等极限词</span>
<span class="line">✅ 详细介绍:无「国家级」「顶级」违禁表述</span>
<span class="line">⚠️ 建议:「金牌导游」表述需补充资质证明材料</span>
<span class="line">⚠️ 建议:「全额退款」承诺需与退改政策页一致</span>
<span class="line">ℹ️ 提示:主图 800×600 以上,避免文字遮挡地标</span>
</div>
<div class="tips-row">
<span class="tip-pill ok-pill">极限词 0 风险</span>
<span class="tip-pill warn-pill">2 项待补充</span>
<span class="tip-pill">审核通过率预估 96%</span>
</div>
</div>`
};
}
};
const actionLabel = {
title: '生成商品标题', intro: '生成详细介绍', detail: '生成商品详情', image: '配图建议',
itinerary: '规划行程', price: '价格库存方案', seo: 'SEO 关键词', check: '合规检查'
};
/* 表单模块内嵌 AI 按钮入口:面板收起时自动展开,再触发对应工具 */
function formAI(type) {
const panel = $('aiPanel');
if (panel.classList.contains('collapsed')) panel.classList.remove('collapsed');
quickAction(type);
}
async function quickAction(type, silent) {
readForm();
if (!silent) addMsg('user', '点击了「' + actionLabel[type] + '」');
const tp = typing();
await delay(900 + Math.random() * 600);
const g = genData[type]();
generated[type] = { data: g.data, used: 0 };
replaceTyping(tp, g.pre + g.card);
}
/* ================= 串行确认机制 ================= */
let serialState = null; // { resolve, timer, msgEl, prefix, index, type, autoMode }
let autoConfirmMode = true; // 默认自动确认
function buildConfirmBar(prefix, index, type, autoMode) {
const hasApply = !!applyMap[type];
const label = hasApply ? '✅ 确认填入' : '✅ 确认';
const cd = autoMode
? `<span class="ag-cd" id="cd${prefix}${index}">5s 后自动确认</span>`
: `<span class="ag-cd manual">⏸ 等待您确认…</span>`;
return `<div class="ag-confirm-bar" id="cb${prefix}${index}">
<button class="ag-cf-btn" onclick="serialConfirm()">${label}</button>
<button class="ag-regen-btn" onclick="serialRegen()">🔄 换一版</button>
${cd}
</div>`;
}
function startSerialCountdown() {
if (!serialState || !serialState.autoMode) return;
let remaining = 5;
const cdEl = $('cd' + serialState.prefix + serialState.index);
serialState.timer = setInterval(() => {
remaining--;
if (remaining <= 0) {
clearInterval(serialState.timer);
serialState.timer = null;
const { resolve } = serialState;
serialState = null;
resolve();
} else if (cdEl) {
cdEl.textContent = remaining + 's 后自动确认';
}
}, 1000);
}
async function showStepCard(prefix, index, type, autoMode) {
const tp = typing();
await delay(500 + Math.random() * 400);
const g = genData[type]();
generated[type] = { data: g.data, used: 0 };
replaceTyping(tp, '<div class="serial-card">' + g.pre + g.card + buildConfirmBar(prefix, index, type, autoMode) + '</div>');
await new Promise(resolve => {
serialState = { resolve, timer: null, msgEl: tp, prefix, index, type, autoMode };
if (autoMode) startSerialCountdown();
});
}
function serialConfirm() {
if (!serialState) return;
if (serialState.timer) { clearInterval(serialState.timer); serialState.timer = null; }
// 标记确认栏为已确认
const bar = $('cb' + serialState.prefix + serialState.index);
if (bar) {
bar.style.opacity = '.55';
bar.innerHTML = '<span style="color:#22c55e;font-size:12px;font-weight:600;">✓ 已确认,填入表单中…</span>';
}
const { resolve } = serialState;
serialState = null;
resolve();
}
function serialRegen() {
if (!serialState) return;
if (serialState.timer) { clearInterval(serialState.timer); serialState.timer = null; }
const { msgEl, prefix, index, type, autoMode } = serialState;
readForm();
const g = genData[type]();
generated[type] = { data: g.data, used: 0 };
replaceTyping(msgEl, '<div class="serial-card">' + g.pre + g.card + buildConfirmBar(prefix, index, type, autoMode) + '</div>');
if (autoMode) startSerialCountdown();
}
function setAutoConfirm(val) {
autoConfirmMode = val;
document.querySelectorAll('.ag-mt-btn').forEach(b => {
b.classList.toggle('active', (b.dataset.auto === 'true') === val);
});
if (!serialState) return;
if (serialState.timer) { clearInterval(serialState.timer); serialState.timer = null; }
serialState.autoMode = val;
// 替换确认栏
const oldBar = $('cb' + serialState.prefix + serialState.index);
if (oldBar) oldBar.outerHTML = buildConfirmBar(serialState.prefix, serialState.index, serialState.type, val);
if (val) startSerialCountdown();
}
/* ================= 自主发品 Agent(对话式多工具编排) ================= */
const agentSteps = [
{ label: '商品标题', type: 'title', note: '3 方案·已选高转化款填入' },
{ label: '详细介绍', type: 'intro', note: '四段式卖点已填入' },
{ label: '商品详情', type: 'detail', note: '图文结构已写入编辑器' },
{ label: '商品图片', type: 'image', note: '6 张配图清单已添加' },
{ label: '行程编辑', type: 'itinerary', note: '逐日行程已填入' },
{ label: '价格库存', type: 'price', note: '5 个团期 + 定价已填入' },
{ label: 'SEO 关键词', type: 'seo', note: '6 个高流量词已提取' },
{ label: '合规检查', type: 'check', note: '扫描完成·0 极限词风险' }
];
/* 从自然语言里提取配置并回写左侧表单 */
function parseBrief(t) {
const o = {};
const destMap = [['云南','云南(昆明/大理/丽江)'],['四川','四川(成都/九寨沟)'],['海南','海南(三亚)'],['三亚','海南(三亚)'],['新疆','新疆(乌鲁木齐/喀纳斯)'],['西藏','西藏(拉萨/林芝)']];
for (const [k, v] of destMap) if (t.includes(k)) { o.dest = v; o.destShort = k === '三亚' ? '海南' : k; break; }
for (const ty of ['跟团游','自由行','定制游','半自助游','当地玩乐']) if (t.includes(ty)) { o.type = ty; break; }
const thMap = [['亲子','亲子家庭'],['蜜月','蜜月情侣'],['银发','银发无忧'],['摄影','摄影爱好'],['小众','小众深度'],['深度','小众深度']];
for (const [k, v] of thMap) if (t.includes(k)) { o.theme = v; break; }
const dm = t.match(/(\d+)\s*[天日]/);
if (dm) {
const n = +dm[1];
o.days = ['3天2晚','5天4晚','6天5晚','7天6晚','8天7晚'].find(d => d.startsWith(n + '天')) || (n + '天' + (n - 1) + '晚');
}
return o;
}
function applyBriefToForm(b) {
if (b.dest) $('f-dest').value = b.dest;
if (b.days) $('f-days').value = b.days;
if (b.type) $('f-type').value = b.type;
if (b.theme) document.querySelectorAll('#f-tags .chip').forEach(x => x.classList.toggle('active', x.textContent === b.theme));
readForm();
}
/* 判断是否为“自主发品”意图 */
const agentIntentRe = /(发品|发布|上架|帮我发|全部生成|全都|六大模块|6大模块|30秒|搞定|一键发)/;
async function agentRun(rawText) {
const b = parseBrief(rawText);
applyBriefToForm(b);
const destShort = ctx.dest.replace(/(.*)/, '');
const ctxLine = `${ctx.dest} · ${ctx.days} · ${ctx.type} · ${ctx.theme}`;
addMsg('ai', `🚀 好的,我来自主完成这次发品。<br>已识别配置:<b>${ctxLine}</b>${b.dest ? '(来自您的描述)' : '(沿用当前表单)'}。<br>我将依次调用 <b>8 个工具</b>,每个工具生成后<b>展示卡片等您确认</b>再填入表单:`);
// 任务清单卡片
const card = addMsg('ai',
`<div class="agent-card">
<div class="ag-head">🤖 自主发品任务清单</div>
<div class="ag-ctx">📋 上下文:${ctxLine} | 预计 8 步</div>
<div class="ag-mode-row">
<span class="ag-mode-label">确认模式</span>
<button class="ag-mt-btn ${autoConfirmMode ? 'active' : ''}" data-auto="true" onclick="setAutoConfirm(true)">⚡ 自动确认</button>
<button class="ag-mt-btn ${!autoConfirmMode ? 'active' : ''}" data-auto="false" onclick="setAutoConfirm(false)">✋ 手动确认</button>
</div>
<div class="ag-steps" id="agSteps">` +
agentSteps.map((s, i) => `<div class="ag-step todo" id="agS${i}"><span class="ag-ico">·</span><span class="ag-lbl">${s.label}</span><span class="ag-note">等待中</span></div>`).join('') +
`</div>
<div class="ag-foot">💡 每个工具生成后展示卡片,自动模式 5s 后自动确认填入,手动模式需您点击确认。可随时切换模式。</div>
</div>`);
for (let i = 0; i < agentSteps.length; i++) {
const s = agentSteps[i];
const el = $('agS' + i);
el.className = 'ag-step doing';
el.querySelector('.ag-ico').textContent = '↻';
el.querySelector('.ag-note').textContent = '生成中…';
await showStepCard('ag', i, s.type, autoConfirmMode);
if (applyMap[s.type]) applyMap[s.type]();
el.className = 'ag-step done';
el.querySelector('.ag-ico').textContent = '✓';
el.querySelector('.ag-note').textContent = s.note;
}
// 沉淀偏好(与一键发品共用记忆,越用越好用)
const newPrefs = [
`${destShort}路线偏好:${ctx.days}${ctx.type}`,
`「${ctx.theme}」人群文案风格`,
'纯玩无购物话术偏好',
'平季低于市场 5% 定价策略',
'首月立减冲榜促销习惯'
];
ocStore.count++;
ocStore.prefs = ocStore.prefs.concat(newPrefs);
ocStore.last = {
dest: destShort, days: ctx.days, type: ctx.type, theme: ctx.theme,
destSel: ctx.dest, daysSel: ctx.days, typeSel: ctx.type,
styleWord: ocStore.count >= 3 ? `${ctx.theme}友好·${ocStore.count >= 10 ? '爆款结构复刻' : '您的惯用话术'}` : '纯玩无购物'
};
saveOC();
$('formArea').scrollTo({ top: 0, behavior: 'smooth' });
const lv = ocLevelInfo();
addMsg('ai', `✅ <b>自主发品完成!</b>6 大模块已全部填入左侧表单,另完成 SEO 关键词提取与合规扫描。<br>本次沉淀偏好 <b>${newPrefs.length} 条</b>,累计发品 <b>${ocStore.count} 次</b>(${lv.name})。<br>可直接点顶部「提交审核」,或对任一模块点「🔄 换一版」微调。`);
}
/* ================= 多工具串行编排(部分模块自主调度) ================= */
const toolKeywords = [
{ type: 'title', kws: ['标题', '名字'] },
{ type: 'intro', kws: ['介绍', '卖点'] },
{ type: 'detail', kws: ['详情'] },
{ type: 'image', kws: ['图', '配图', '拍摄'] },
{ type: 'itinerary', kws: ['行程', '路线', '规划'] },
{ type: 'price', kws: ['价', '定价', '库存', '团期', '多少钱'] },
{ type: 'seo', kws: ['seo', '关键词', '搜索'] },
{ type: 'check', kws: ['合规', '违禁', '广告法'] },
];
/* 从自然语言中提取用户提到的工具(去重、保序) */
function parseMultiTools(text) {
const lower = text.toLowerCase();
const found = [], seen = new Set();
for (const { type, kws } of toolKeywords) {
if (seen.has(type)) continue;
if (kws.some(k => lower.includes(k))) { found.push(type); seen.add(type); }
}
return found;
}
/* 多工具串行编排:仅跑用户指定的模块,非全量发品 */
async function multiToolRun(rawText, toolTypes) {
const b = parseBrief(rawText);
if (Object.keys(b).length) applyBriefToForm(b);
const destShort = ctx.dest.replace(/(.*)/, '');
const ctxLine = `${ctx.dest} · ${ctx.days} · ${ctx.type} · ${ctx.theme}`;
const steps = agentSteps.filter(s => toolTypes.includes(s.type));
const labels = steps.map(s => s.label).join(' → ');
addMsg('ai', `🛠️ 好的,我来自主调度 <b>${steps.length}</b> 个模块。<br>已识别上下文:<b>${ctxLine}</b>${b.dest ? '(来自您的描述)' : '(沿用当前表单)'}。<br>将依次执行:<b>${labels}</b>,每个工具生成后<b>展示卡片等您确认</b>再填入表单:`);
// 任务清单卡片
const card = addMsg('ai',
`<div class="agent-card">
<div class="ag-head">🛠️ 多工具串行任务</div>
<div class="ag-ctx">📋 ${ctxLine} | 共 ${steps.length} 步</div>
<div class="ag-mode-row">
<span class="ag-mode-label">确认模式</span>
<button class="ag-mt-btn ${autoConfirmMode ? 'active' : ''}" data-auto="true" onclick="setAutoConfirm(true)">⚡ 自动确认</button>
<button class="ag-mt-btn ${!autoConfirmMode ? 'active' : ''}" data-auto="false" onclick="setAutoConfirm(false)">✋ 手动确认</button>
</div>
<div class="ag-steps" id="mtSteps">` +
steps.map((s, i) => `<div class="ag-step todo" id="mtS${i}"><span class="ag-ico">·</span><span class="ag-lbl">${s.label}</span><span class="ag-note">等待中</span></div>`).join('') +
`</div>
<div class="ag-foot">💡 每个工具生成后展示卡片,自动模式 5s 后自动确认填入,手动模式需您点击确认。可随时切换模式。</div>
</div>`);
for (let i = 0; i < steps.length; i++) {
const s = steps[i];
const el = $('mtS' + i);
el.className = 'ag-step doing';
el.querySelector('.ag-ico').textContent = '↻';
el.querySelector('.ag-note').textContent = '生成中…';
await showStepCard('mt', i, s.type, autoConfirmMode);
if (applyMap[s.type]) applyMap[s.type]();
el.className = 'ag-step done';
el.querySelector('.ag-ico').textContent = '✓';
el.querySelector('.ag-note').textContent = s.note;
}
// 滚动到第一个修改的表单模块
const firstType = steps[0].type;
const cardMap = { title: 'card-title', intro: 'card-intro', detail: 'card-detail', image: 'card-img', itinerary: 'card-itinerary', price: 'card-price' };
if (cardMap[firstType]) $(cardMap[firstType]).scrollIntoView({ behavior: 'smooth', block: 'center' });
addMsg('ai', `✅ <b>${steps.length} 个模块生成完毕!</b>${steps.map(s => s.label).join('、')} 已填入左侧表单。<br>不满意可点对应模块的「🔄 换一版」微调,或说「<b>帮我把这个商品发布了</b>」让我一口气完成全部模块。`);
}
/* ================= 自由对话 ================= */
const canned = [
{ k: ['标题', '名字'], f: () => quickAction('title') },
{ k: ['介绍', '卖点'], f: () => quickAction('intro') },
{ k: ['详情'], f: () => quickAction('detail') },
{ k: ['图', '配图', '拍摄'], f: () => quickAction('image') },
{ k: ['行程', '规划', '路线'], f: () => quickAction('itinerary') },
{ k: ['价', '定价', '库存', '团期', '多少钱'], f: () => quickAction('price') },
{ k: ['seo', '关键词', '搜索'], f: () => quickAction('seo') },
{ k: ['合规', '违禁', '广告法'], f: () => quickAction('check') },
];
async function sendMessage() {
const input = $('chatInput');
const text = input.value.trim();
if (!text) return;
// 如果有串行步骤在等待确认,自动确认后继续处理新消息
if (serialState) serialConfirm();
input.value = '';
addMsg('user', esc(text));
const tp = typing();
await delay(700);
// 优先:多工具串行(用户同时提到 2+ 模块)
const tools = parseMultiTools(text);
if (tools.length >= 2) {
replaceTyping(tp, `明白,我来自主调度 ${tools.length} 个模块 🛠️`);
await delay(400);
multiToolRun(text, tools);
return;
}
// 全量自主发品
if (agentIntentRe.test(text)) {
replaceTyping(tp, '明白,我来自主完成这次发品 🚀');
await delay(400);
agentRun(text);
return;
}
const hit = canned.find(c => c.k.some(k => text.toLowerCase().includes(k)));
if (hit) {
replaceTyping(tp, '好的,马上为您处理 👇');
await delay(400);
hit.f();
return;
}
const replies = [
`收到!我已读取当前表单:目的地 <span class="hl">${ctx.dest}</span>、类型 <span class="hl">${ctx.type}</span>。您可以点上方快捷按钮生成内容,或直接说需求,例如:<br>·「帮我写个适合${ctx.theme}的标题」<br>·「国庆团期怎么定价」`,
`关于「${esc(text.slice(0, 12))}${text.length > 12 ? '…' : ''}」,建议先在左侧表单选好目的地和天数,我生成的内容会更精准。也可以试试快捷区的 <b>价格库存</b>,我会基于市场数据给出排期方案~`,
`我可以帮您拆解:先定目标人群(${ctx.theme}?蜜月?),再定核心卖点(纯玩/性价比/体验感),然后按框架生成标题→介绍→详情→行程。直接点快捷按钮即可开始 ✨`,
];
replaceTyping(tp, replies[Math.floor(Math.random() * replies.length)]);
}
/* ================= 标题计数 / 标签选择 ================= */
$('f-title').addEventListener('input', function () {
$('titleCount').textContent = this.value.length;
});
document.querySelectorAll('#f-tags .chip').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#f-tags .chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
});
});
document.querySelectorAll('#oc-tags .chip').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('#oc-tags .chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
});
});
/* ================= 面板开关 ================= */
function togglePanel() { $('aiPanel').classList.toggle('collapsed'); }
/* ================= 一键发品(越用越好用) ================= */
let ocStore = { count: 0, prefs: [], last: null };
try { ocStore = JSON.parse(localStorage.getItem('fliggy_oc') || 'null') || ocStore; } catch (e) {}
function saveOC() { try { localStorage.setItem('fliggy_oc', JSON.stringify(ocStore)); } catch (e) {} }
function ocLevelInfo() {
if (ocStore.count >= 10) return { name: 'Lv.3 爆款复刻', next: '已解锁:自动复用您的历史爆款结构与定价', pct: 100 };
if (ocStore.count >= 3) return { name: 'Lv.2 风格匹配', next: '再用 ' + (10 - ocStore.count) + ' 次解锁「Lv.3 爆款复刻」', pct: ocStore.count / 10 * 100 };
return { name: 'Lv.1 通用模板', next: '再用 ' + (3 - ocStore.count) + ' 次解锁「Lv.2 风格匹配」(生成将贴合您的风格)', pct: ocStore.count / 10 * 100 };
}
function openOC() {
// 按学习到的偏好预填
const last = ocStore.last;
if (last) {
$('oc-dest').value = last.destSel || $('oc-dest').value;
$('oc-days').value = last.daysSel || $('oc-days').value;
$('oc-type').value = last.typeSel || $('oc-type').value;
document.querySelectorAll('#oc-tags .chip').forEach(c =>
c.classList.toggle('active', c.textContent === last.theme));
}
const hint = $('ocLearn');
if (ocStore.count > 0 && last) {
hint.style.display = 'block';
hint.innerHTML = `🧠 <b>越用越好用</b>:已根据您最近 ${ocStore.count} 次发品习惯<b>自动预填</b>(${last.dest}·${last.days}·${last.type}·${last.theme}),生成内容将沿用您偏好的「${last.styleWord}」风格。`;
} else {
hint.style.display = 'none';
}
const lv = ocLevelInfo();
$('ocCount').textContent = ocStore.count;
$('ocPrefN').textContent = ocStore.prefs.length;
$('ocLevel').textContent = lv.name;
$('ocLevelNext').textContent = lv.next;
$('ocBar').style.width = lv.pct + '%';
$('ocConfig').style.display = 'block';
$('ocProgress').style.display = 'none';
$('ocDone').style.display = 'none';
$('ocMask').classList.add('show');
}
function closeOC() { $('ocMask').classList.remove('show'); }
const ocStepsDef = [
{ label: '商品标题', type: 'title', note: '已选高转化方案' },
{ label: '详细介绍', type: 'intro', note: '四段式卖点' },
{ label: '商品详情', type: 'detail', note: '图文结构就绪' },
{ label: '商品图片', type: 'image', note: '配图清单 6 张' },
{ label: '行程编辑', type: 'itinerary',note: '逐日框架就绪' },
{ label: '价格库存', type: 'price', note: '团期+定价就绪' }
];
async function startOC() {
const destSel = $('oc-dest').value, daysSel = $('oc-days').value, typeSel = $('oc-type').value;
const themeEl = document.querySelector('#oc-tags .chip.active');
const theme = themeEl ? themeEl.textContent : '亲子家庭';
// 同步配置到左侧表单(生成依赖表单上下文)
$('f-dest').value = destSel;
$('f-days').value = daysSel;
$('f-type').value = typeSel;
document.querySelectorAll('#f-tags .chip').forEach(x =>
x.classList.toggle('active', x.textContent === theme));
$('ocConfig').style.display = 'none';
$('ocProgress').style.display = 'block';
$('ocSteps').innerHTML = ocStepsDef.map((s, i) =>
`<div class="step-item" id="ocS${i}"><span class="s-icon">·</span><span>${s.label}</span><span class="s-note">等待中</span></div>`).join('');
silentBatch = true;
for (let i = 0; i < ocStepsDef.length; i++) {
const s = ocStepsDef[i];
const el = $('ocS' + i);
el.className = 'step-item doing';
el.querySelector('.s-icon').textContent = '↻';
el.querySelector('.s-note').textContent = '生成中…';
await delay(650 + Math.random() * 450);
const g = genData[s.type]();
generated[s.type] = { data: g.data, used: 0 };
applyMap[s.type]();
el.className = 'step-item done';
el.querySelector('.s-icon').textContent = '✓';
el.querySelector('.s-note').textContent = s.note;
}
silentBatch = false;
// 沉淀偏好(越用越好好的核心)
const dest = destSel.replace(/(.*)/, '');
const newPrefs = [
`${dest}路线偏好:${daysSel}${typeSel}`,
`「${theme}」人群文案风格`,
'纯玩无购物话术偏好',
'平季低于市场 5% 定价策略',
'首月立减冲榜促销习惯'
];
ocStore.count++;
ocStore.prefs = ocStore.prefs.concat(newPrefs);
ocStore.last = {
dest, days: daysSel, type: typeSel, theme,
destSel, daysSel, typeSel,
styleWord: ocStore.count >= 3 ? `${theme}友好·${ocStore.count >= 10 ? '爆款结构复刻' : '您的惯用话术'}` : '纯玩无购物'
};
saveOC();
$('ocProgress').style.display = 'none';
$('ocDone').style.display = 'block';
const lv = ocLevelInfo();
$('ocNewPref').innerHTML = `🧠 <b>本次为您沉淀 ${newPrefs.length} 条偏好</b>(累计 ${ocStore.prefs.length} 条):<br>` +
newPrefs.map(p => '· ' + p).join('<br>') +
`<br><b>${lv.name}</b> · ${lv.next}<br>下次一键发品将自动预填并按您的风格生成。`;
$('formArea').scrollTo({ top: 0, behavior: 'smooth' });
toast('6 大模块已生成完毕');
addMsg('ai', `已完成 <b>一键发品</b> ✅ 标题、介绍、详情、配图、行程、价格库存全部就位。<br>本次沉淀偏好 <b>${ocStore.prefs.length} 条</b>,累计发品 <b>${ocStore.count} 次</b>——用得越多,生成越贴合您的风格。<br>任何模块不满意,点对应快捷按钮「🔄 换一版」即可。`);
}
/* ================= 初始化 ================= */
renderImages();
renderItinerary();
renderPrice();
window.addEventListener('load', async () => {
const tp1 = addMsg('ai', '<div class="typing"><span></span><span></span><span></span></div>');
await delay(700);
replaceTyping(tp1, `您好,我是 <b>旅智小助</b> 🤖<br>我对应左侧 6 大表单模块,可以帮您:<br>· 📝 商品标题 — 3 个方案一键切换填入<br>· 💎 详细介绍 — 四段式卖点文案<br>· 📄 商品详情 — 结构化图文(含配图占位)<br>· 🖼️ 配图建议 — 6 张图拍摄脚本<br>· 🗺️ 行程编辑 — 逐日行程一键填入<br>· 💰 价格库存 — 团期排期 + 定价策略<br><br>💬 <b>自主发品</b>:说「<b>帮我优化标题和详情</b>」或「<b>帮我发个云南亲子 6 天跟团游</b>」,我会<b>逐个工具生成 → 展示卡片 → 等您确认</b>再填入表单(⚡自动 5s 确认,可切✋手动)~`);
await delay(600);
const tp2 = typing();
await delay(900);
replaceTyping(tp2, `💡 <b>演示提示</b>:<br>· 说「<b>帮我优化标题和详情</b>」→ 串行调度指定模块<br>· 说「<b>帮我发个海南亲子 6 天自由行</b>」→ 自主调用全部 8 个工具<br>· 每步<b>生成卡片 → 等您确认</b>再填入(⚡自动 5s / ✋手动可随时切换)<br>· 也可点 <b>⚡ 一键发品</b> 用弹窗方式完成<br>都会沉淀偏好,<b>用得越多越懂你</b>。`);
});
</script>
</body>
</html>