index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  1. <template>
  2. <div class="ai-page ai-expert">
  3. <!-- 数据来源入口(设计稿 hero 区):「上传文件」按历史逻辑跳转上传数据页;「开始分析」=新建对话并绑定默认内置研判方案 -->
  4. <section class="ai-expert__hero-grid">
  5. <article class="ai-expert__hero">
  6. <div class="ai-expert__hero-icon"><Icon icon="mdi:upload" :size="22" /></div>
  7. <div class="ai-expert__hero-body">
  8. <div class="ai-expert__hero-title">数据清洗与治理</div>
  9. <p class="ai-expert__hero-desc"
  10. >上传案件文件后自动执行清洗与治理,形成规范、可分析的数据,供后续智能研判使用。</p
  11. >
  12. </div>
  13. <button class="ai-expert__hero-start" type="button" @click="onUploadFile">
  14. 上传文件
  15. <Icon icon="mdi:arrow-right" :size="15" />
  16. </button>
  17. </article>
  18. <article class="ai-expert__hero">
  19. <div class="ai-expert__hero-icon"><Icon icon="mdi:filter-variant" :size="22" /></div>
  20. <div class="ai-expert__hero-body">
  21. <div class="ai-expert__hero-title">基于清洗结果智能分析</div>
  22. <p class="ai-expert__hero-desc">
  23. 基于当前案件上传数据经模板库自动清洗治理后的结果,通过系统内置的研判方案进行分析。
  24. </p>
  25. </div>
  26. <button class="ai-expert__hero-start" type="button" @click="onCleanAnalysis">
  27. 开始分析
  28. <Icon icon="mdi:arrow-right" :size="15" />
  29. </button>
  30. </article>
  31. </section>
  32. <!-- 面板:顶栏(标题 + 摘要胶囊 + 搜索 + 操作)+ 提示 + 研判方案卡片网格 -->
  33. <section class="ai-expert__panel">
  34. <div class="ai-expert__bar">
  35. <span class="ai-expert__title">研判方案</span>
  36. <span class="ai-expert__count">{{ summary }}</span>
  37. <label class="ai-expert__search">
  38. <Icon icon="mdi:magnify" :size="14" />
  39. <input v-model="keyword" type="text" placeholder="输入方案名称搜索" />
  40. </label>
  41. <span class="ai-expert__bar-sp"></span>
  42. <a-button :loading="loading" @click="load">
  43. <template #icon><Icon icon="mdi:refresh" :size="14" /></template>
  44. 刷新
  45. </a-button>
  46. <a-button type="primary" @click="openCreate">
  47. <template #icon><Icon icon="mdi:plus" :size="14" /></template>
  48. 新建研判方案
  49. </a-button>
  50. </div>
  51. <a-spin :spinning="loading">
  52. <!-- 卡片混合展示(内置在前),右上角标签区分内置 / 自定义 -->
  53. <div v-if="filteredExperts.length" class="ai-expert__grid">
  54. <article v-for="e in filteredExperts" :key="e.id" class="ai-expert__card">
  55. <div class="ai-expert__card-head">
  56. <span class="ai-expert__card-icon">{{ (e.name || '?').charAt(0) }}</span>
  57. <span class="ai-expert__name" :title="e.name">{{ e.name }}</span>
  58. </div>
  59. <span class="ai-expert__badge" :class="e.builtin ? 'is-builtin' : 'is-custom'">
  60. {{ e.builtin ? '内置' : '自定义' }}
  61. </span>
  62. <p class="ai-expert__desc">{{ e.description || '暂无描述' }}</p>
  63. <div class="ai-expert__chips">
  64. <span class="ai-expert__chip">
  65. <i class="ai-expert__chip-dot" style="background: #0969da"></i>
  66. {{ groupSummary(e) }}
  67. </span>
  68. <span class="ai-expert__chip">
  69. <i class="ai-expert__chip-dot" style="background: #8250df"></i>
  70. {{ skillSummary(e) }}
  71. </span>
  72. </div>
  73. <div class="ai-expert__card-ops">
  74. <!-- 「开始分析」视觉取设计稿的主色文字按钮,点击行为不变(emit chat 进入新对话草稿) -->
  75. <button class="ai-expert__run" type="button" @click="onChat(e)">
  76. 开始分析
  77. <Icon icon="mdi:arrow-right" :size="13" />
  78. </button>
  79. <span class="ai-expert__card-ops-sp"></span>
  80. <!-- 查看详情:内置/自定义都可看(只读弹框),放最左与编辑/删除分隔开 -->
  81. <a-tooltip title="查看详情">
  82. <button
  83. class="ai-expert__iconbtn"
  84. type="button"
  85. aria-label="查看方案详情"
  86. @click="openDetail(e)"
  87. >
  88. <Icon icon="mdi:eye-outline" :size="15" />
  89. </button>
  90. </a-tooltip>
  91. <!-- 内置只可对话(编辑/删除入口根本不渲染,后端另有双重校验) -->
  92. <template v-if="!e.builtin">
  93. <a-tooltip title="编辑研判方案">
  94. <button class="ai-expert__iconbtn" type="button" aria-label="编辑研判方案" @click="openEdit(e)">
  95. <Icon icon="mdi:pencil-outline" :size="15" />
  96. </button>
  97. </a-tooltip>
  98. <a-tooltip title="删除研判方案">
  99. <button
  100. class="ai-expert__iconbtn is-danger"
  101. type="button"
  102. aria-label="删除研判方案"
  103. @click="onDelete(e)"
  104. >
  105. <Icon icon="mdi:trash-can-outline" :size="15" />
  106. </button>
  107. </a-tooltip>
  108. </template>
  109. </div>
  110. </article>
  111. </div>
  112. <div v-else-if="!loading && !experts.length" class="ai-expert__empty">
  113. 还没有研判方案。点击右上角「新建研判方案」创建第一个。
  114. </div>
  115. <div v-else-if="!loading" class="ai-expert__empty"> 没有找到与「{{ keyword.trim() }}」匹配的研判方案。 </div>
  116. </a-spin>
  117. </section>
  118. <!-- 新建/编辑:默认窗口弹框(可右上角全屏),内容为新建方案页设计版式 -->
  119. <AgentFormModal v-model:open="formOpen" :record="editing" @saved="onSaved" />
  120. <!-- 方案详情(只读):卡片眼睛图标打开,展示提示词与勾选的工具包/技能 -->
  121. <BasicModal
  122. :open="detailOpen"
  123. :title="detail ? `方案详情:${detail.name}` : '方案详情'"
  124. :width="760"
  125. :show-ok-btn="false"
  126. cancel-text="关闭"
  127. wrap-class-name="ae-detail-modal"
  128. @cancel="detailOpen = false"
  129. >
  130. <div v-if="detail" class="ae-detail">
  131. <div class="ae-detail__head">
  132. <span class="ae-detail__badge" :class="detail.builtin ? 'is-builtin' : 'is-custom'">
  133. {{ detail.builtin ? '内置' : '自定义' }}
  134. </span>
  135. <span class="ae-detail__name">{{ detail.name }}</span>
  136. </div>
  137. <p class="ae-detail__desc">{{ detail.description || '暂无描述' }}</p>
  138. <section class="ae-detail__panel">
  139. <div class="ae-detail__label">系统提示词</div>
  140. <pre class="ae-detail__prompt">{{ detail.sysPrompt || '(未填写)' }}</pre>
  141. </section>
  142. <section class="ae-detail__panel">
  143. <div class="ae-detail__label">可用工具包</div>
  144. <div v-if="detailToolChips.length" class="ae-detail__chips">
  145. <span v-for="n in detailToolChips" :key="n" class="ae-detail__chip">{{ n }}</span>
  146. </div>
  147. <div v-else class="ae-detail__none">不限制(全部业务工具可用)</div>
  148. </section>
  149. <section class="ae-detail__panel">
  150. <div class="ae-detail__label">可用技能</div>
  151. <div v-if="(detail.skillNames || []).length" class="ae-detail__chips">
  152. <span v-for="n in detail.skillNames" :key="n" class="ae-detail__chip">{{ n }}</span>
  153. </div>
  154. <div v-else class="ae-detail__none">不限制(该账号安装的技能全部可用)</div>
  155. </section>
  156. </div>
  157. </BasicModal>
  158. </div>
  159. </template>
  160. <script lang="ts" setup name="AiExpertIndex">
  161. /**
  162. * 研判方案(智能体)管理页:卡片混合列表,右上角标签区分「内置 / 自定义」。
  163. *
  164. * 对接 AgentExpertController(/agents/**,Result 包装):列表本身就是「内置 + 自己的」
  165. * (后端过滤,内置在前),页面不再分组,只靠卡片角标区分来源。
  166. *
  167. * 操作语义:
  168. * - 「上传文件」入口:按历史逻辑跳转上传数据页(CaseDataUpload,/data/upload 全屏流程首步);
  169. * - 「开始分析」(第二块卡):=新建对话逻辑,预选默认内置研判方案绑定(复用研判方案卡的 chat 事件);
  170. * - 开始分析(研判方案卡):向上层 emit(工作台切到对话视图并预选该研判方案,下一条消息创建的会话即绑定它);
  171. * - 编辑/删除:仅自建卡片出现,内置的按钮根本不渲染(后端另有双重校验)。
  172. */
  173. import { computed, onMounted, ref } from 'vue';
  174. import { useRouter } from 'vue-router';
  175. import { Button, Modal, Spin, Tooltip } from 'ant-design-vue';
  176. import Icon from '@/core/components/Icon/src/Icon.vue';
  177. import { useMessage } from '@/core/hooks/web/useMessage';
  178. import { BasicModal } from '@/core/components/Modal';
  179. import * as agentApi from '../../api/agentApi';
  180. import * as toolPackApi from '../../api/toolPackApi';
  181. import type { AiExpert, ToolPack } from '../../api/types';
  182. import AgentFormModal from '../../components/AgentFormModal.vue';
  183. import { toolGroupLabel } from '../../utils/constants';
  184. const AButton = Button;
  185. const ASpin = Spin;
  186. const ATooltip = Tooltip;
  187. const emit = defineEmits<{
  188. chat: [expert: AiExpert];
  189. }>();
  190. const { showMessage } = useMessage();
  191. const router = useRouter();
  192. const experts = ref<AiExpert[]>([]);
  193. const loading = ref(false);
  194. const formOpen = ref(false);
  195. const editing = ref<AiExpert | null>(null);
  196. /** 方案详情(只读弹框) */
  197. const detailOpen = ref(false);
  198. const detail = ref<AiExpert | null>(null);
  199. /** 工具包清单(id → 名,懒加载一次;详情弹框把 toolPackIds 显示成人话) */
  200. const packCatalog = ref<ToolPack[] | null>(null);
  201. async function openDetail(e: AiExpert) {
  202. detail.value = e;
  203. detailOpen.value = true;
  204. if (!packCatalog.value) {
  205. packCatalog.value = (await toolPackApi.listToolPacks().catch(() => [])) || [];
  206. }
  207. }
  208. /** 详情里的工具包 chip:内置/自建包按 id 取名;旧方案残留的工具组按组名显示;去重防同名并列 */
  209. const detailToolChips = computed(() => {
  210. const d = detail.value;
  211. if (!d) return [];
  212. const chips = new Set<string>();
  213. for (const id of d.toolPackIds || []) {
  214. chips.add(packCatalog.value?.find((p) => p.id === id)?.name || `工具包 ${id}`);
  215. }
  216. for (const g of d.toolGroups || []) chips.add(toolGroupLabel(g));
  217. return [...chips];
  218. });
  219. /** 研判方案名称搜索(纯前端过滤,不改接口与加载逻辑) */
  220. const keyword = ref('');
  221. const filteredExperts = computed(() => {
  222. const k = keyword.value.trim().toLowerCase();
  223. if (!k) return experts.value;
  224. return experts.value.filter((e) => (e.name || '').toLowerCase().includes(k));
  225. });
  226. const builtinExperts = computed(() => experts.value.filter((e) => e.builtin));
  227. const mineExperts = computed(() => experts.value.filter((e) => !e.builtin));
  228. const summary = computed(() => {
  229. const parts: string[] = [];
  230. if (builtinExperts.value.length) parts.push(`内置 ${builtinExperts.value.length}`);
  231. if (mineExperts.value.length) parts.push(`我的 ${mineExperts.value.length}`);
  232. return parts.length ? parts.join(' · ') : '尚未创建研判方案';
  233. });
  234. onMounted(load);
  235. async function load() {
  236. loading.value = true;
  237. try {
  238. experts.value = (await agentApi.listExperts()) || [];
  239. } catch (e: any) {
  240. showMessage(e?.message || '加载研判方案列表失败', 'error');
  241. } finally {
  242. loading.value = false;
  243. }
  244. }
  245. /** 工具摘要:勾了工具包显示包数,旧方案还有工具组的列出组名,都没有 = 不限制 */
  246. function groupSummary(e: AiExpert): string {
  247. const groups = e.toolGroups || [];
  248. const packIds = e.toolPackIds || [];
  249. if (!groups.length && !packIds.length) return '工具:不限制';
  250. const parts: string[] = [];
  251. if (packIds.length) parts.push(`${packIds.length} 个工具包`);
  252. if (groups.length) parts.push(groups.map((g) => toolGroupLabel(g)).join('、'));
  253. return `工具:${parts.join(' + ')}`;
  254. }
  255. function skillSummary(e: AiExpert): string {
  256. const names = e.skillNames || [];
  257. if (!names.length) return '技能:不限制';
  258. return `技能:${names.length} 个已绑定`;
  259. }
  260. function onChat(e: AiExpert) {
  261. emit('chat', e);
  262. }
  263. /**
  264. * 「开始分析」(基于清洗结果):=新建对话的逻辑,用默认内置研判方案绑定——
  265. * 复用研判方案卡的 chat 事件(工作台 startDraft 预选该研判方案 → 切对话视图 → 首条消息创建会话即绑定)。
  266. * 内置研判方案列表按后端约定在最前,取第一个 builtin 即默认内置研判方案。
  267. */
  268. function onCleanAnalysis() {
  269. const builtin = experts.value.find((e) => e.builtin);
  270. if (builtin) emit('chat', builtin);
  271. }
  272. /**
  273. * 「上传文件」入口:跳转上传数据页(上传 → 清洗 → 导入全屏流程的首步)。
  274. * 带 redirect 来源地址:上传页的「返回」按它回到本界面,view=agent 让工作台恢复到研判方案视图。
  275. */
  276. function onUploadFile() {
  277. router.push({ name: 'CaseDataUpload', query: { redirect: '/workbench?view=agent' } });
  278. }
  279. function openCreate() {
  280. editing.value = null;
  281. formOpen.value = true;
  282. }
  283. function openEdit(e: AiExpert) {
  284. editing.value = e;
  285. formOpen.value = true;
  286. }
  287. async function onSaved() {
  288. await load();
  289. }
  290. function onDelete(e: AiExpert) {
  291. Modal.confirm({
  292. title: '删除研判方案',
  293. content: `确认删除「${e.name}」?绑定它的会话将无法继续对话(会得到「智能体不存在或已删除」错误)。`,
  294. okText: '删除',
  295. okType: 'primary',
  296. okButtonProps: { danger: true },
  297. cancelText: '取消',
  298. onOk: async () => {
  299. try {
  300. await agentApi.deleteExpert(e.id);
  301. showMessage('已删除', 'success');
  302. await load();
  303. } catch (err: any) {
  304. showMessage(err?.message || '删除失败', 'error');
  305. }
  306. },
  307. });
  308. }
  309. </script>
  310. <style lang="less" scoped>
  311. .ai-expert {
  312. display: flex;
  313. flex-direction: column;
  314. gap: var(--ai-sp-3);
  315. /* 页级内边距与现状一致(8px),四边不贴边 —— 不用设计稿的宽页边距 */
  316. padding: var(--ai-sp-2);
  317. /* 设计稿是灰底白卡:工作台中栏直接给灰色画布,卡片才能像设计稿一样立起来 */
  318. background: var(--ai-bg-page);
  319. /* ── 数据来源入口(设计稿 hero 区) ── */
  320. &__hero-grid {
  321. display: grid;
  322. grid-template-columns: 1fr 1fr;
  323. gap: var(--ai-sp-3);
  324. flex: 0 0 auto;
  325. }
  326. &__hero {
  327. position: relative;
  328. display: flex;
  329. align-items: center;
  330. gap: 14px;
  331. padding: 18px 20px;
  332. overflow: hidden;
  333. background: var(--ai-bg-card, #fff);
  334. border: 1px solid var(--ai-border);
  335. border-radius: var(--ai-radius-lg);
  336. box-shadow: var(--ai-shadow-sm);
  337. transition:
  338. box-shadow 0.15s ease,
  339. transform 0.15s ease;
  340. &:hover {
  341. box-shadow: var(--ai-shadow);
  342. transform: translateY(-1px);
  343. }
  344. /* 角部淡蓝光晕:只做一点品牌氛围,保持克制 */
  345. &::after {
  346. content: '';
  347. position: absolute;
  348. top: -60px;
  349. right: -40px;
  350. width: 220px;
  351. height: 160px;
  352. background: radial-gradient(closest-side, fade(@primary-color, 7%), transparent);
  353. pointer-events: none;
  354. }
  355. }
  356. &__hero-icon {
  357. display: flex;
  358. align-items: center;
  359. justify-content: center;
  360. flex: 0 0 auto;
  361. width: 46px;
  362. height: 46px;
  363. color: var(--ai-primary);
  364. background: var(--ai-primary-soft);
  365. border-radius: 10px;
  366. }
  367. &__hero-body {
  368. flex: 1 1 auto;
  369. min-width: 0;
  370. }
  371. &__hero-title {
  372. font-size: 16px;
  373. font-weight: 700;
  374. letter-spacing: 0.01em;
  375. color: var(--ai-text);
  376. }
  377. &__hero-desc {
  378. margin-top: 5px;
  379. font-size: 12.5px;
  380. line-height: 1.7;
  381. color: var(--ai-text-2);
  382. }
  383. &__hero-start {
  384. position: relative;
  385. z-index: 1;
  386. display: inline-flex;
  387. align-items: center;
  388. gap: 7px;
  389. flex: 0 0 auto;
  390. padding: 0 16px;
  391. height: 36px;
  392. font-size: 13px;
  393. font-weight: 600;
  394. font-family: inherit;
  395. color: #fff;
  396. background: var(--ai-primary);
  397. border: none;
  398. border-radius: var(--ai-radius);
  399. box-shadow: 0 2px 6px fade(@primary-color, 28%);
  400. cursor: pointer;
  401. transition: background 0.15s ease;
  402. &:hover {
  403. background: darken(@primary-color, 8%);
  404. }
  405. }
  406. /* ── 面板(设计稿 panel:白卡承载标题行 + 提示 + 卡片网格) ── */
  407. &__panel {
  408. display: flex;
  409. flex-direction: column;
  410. gap: var(--ai-sp-3);
  411. flex: 1 1 auto;
  412. min-height: 0;
  413. padding: 12px;
  414. background: var(--ai-bg-card, #fff);
  415. border: 1px solid var(--ai-border);
  416. border-radius: var(--ai-radius-lg);
  417. }
  418. &__bar {
  419. display: flex;
  420. align-items: center;
  421. gap: var(--ai-sp-3);
  422. flex: 0 0 auto;
  423. }
  424. &__title {
  425. font-size: 15px;
  426. font-weight: 700;
  427. letter-spacing: 0.01em;
  428. color: var(--ai-text);
  429. }
  430. /* 摘要胶囊(设计稿 panel-head 的 count pill 形态),文案仍是研判方案页原有 summary */
  431. &__count {
  432. /* 上下不对称内边距:胶囊文字墨迹比同行标题/按钮低 1px,据此上移对齐同一光学中线 */
  433. padding: 1px 10px 3px;
  434. font-size: 11.5px;
  435. font-weight: 500;
  436. color: var(--ai-text-2);
  437. background: var(--ai-bg-subtle);
  438. border-radius: 99px;
  439. }
  440. /* 研判方案名称搜索(设计稿 panel-head 的 search 形态) */
  441. &__search {
  442. display: flex;
  443. align-items: center;
  444. gap: 8px;
  445. width: 240px;
  446. height: 32px;
  447. padding: 0 11px;
  448. color: var(--ai-text-3);
  449. background: var(--ai-bg-card, #fff);
  450. border: 1px solid var(--ai-border-strong);
  451. border-radius: var(--ai-radius);
  452. cursor: text;
  453. transition:
  454. border-color 0.15s ease,
  455. box-shadow 0.15s ease;
  456. &:focus-within {
  457. border-color: var(--ai-primary);
  458. box-shadow: 0 0 0 3px var(--ai-primary-soft);
  459. }
  460. input {
  461. flex: 1;
  462. min-width: 0;
  463. font-size: 12.5px;
  464. font-family: inherit;
  465. color: var(--ai-text);
  466. background: transparent;
  467. border: none;
  468. outline: none;
  469. /* 占位符文字用灰色(--ai-text-3 未在 token 中定义,直接写色值) */
  470. &::placeholder {
  471. color: #9aa1ac;
  472. }
  473. }
  474. }
  475. &__bar-sp {
  476. flex: 1 1 auto;
  477. }
  478. &__grid {
  479. display: grid;
  480. grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  481. gap: var(--ai-sp-3);
  482. }
  483. &__card {
  484. position: relative;
  485. display: flex;
  486. flex-direction: column;
  487. gap: 8px;
  488. padding: 14px 14px 10px;
  489. background: var(--ai-bg-card, #fff);
  490. border: 1px solid var(--ai-border);
  491. border-radius: var(--ai-radius);
  492. transition:
  493. box-shadow 0.15s ease,
  494. transform 0.15s ease,
  495. border-color 0.15s ease;
  496. &:hover {
  497. border-color: fade(@primary-color, 35%);
  498. box-shadow: var(--ai-shadow, 0 4px 16px rgba(15, 23, 42, 0.08));
  499. transform: translateY(-1px);
  500. }
  501. }
  502. &__card-head {
  503. display: flex;
  504. align-items: center;
  505. gap: 9px;
  506. /* 给右上角「内置 / 自定义」角标让位 */
  507. padding-right: 52px;
  508. }
  509. /* 设计稿卡片头的图标位:用研判方案名称第一个字作图标 */
  510. &__card-icon {
  511. display: flex;
  512. align-items: center;
  513. justify-content: center;
  514. flex: 0 0 auto;
  515. width: 30px;
  516. height: 30px;
  517. font-size: 15px;
  518. font-weight: 600;
  519. line-height: 1;
  520. color: var(--ai-primary);
  521. background: var(--ai-primary-soft);
  522. border-radius: var(--ai-radius);
  523. }
  524. &__name {
  525. overflow: hidden;
  526. font-size: 13.5px;
  527. font-weight: 700;
  528. color: var(--ai-text);
  529. white-space: nowrap;
  530. text-overflow: ellipsis;
  531. letter-spacing: 0.01em;
  532. }
  533. /* 右上角来源标签:内置(全局)/ 自定义(我的) */
  534. &__badge {
  535. position: absolute;
  536. top: 12px;
  537. right: 12px;
  538. padding: 0 7px;
  539. font-size: 11px;
  540. line-height: 18px;
  541. border-radius: 99px;
  542. &.is-builtin {
  543. color: var(--ai-text-2);
  544. background: var(--ai-bg-subtle);
  545. }
  546. &.is-custom {
  547. color: var(--ai-primary);
  548. background: var(--ai-primary-soft);
  549. }
  550. }
  551. &__desc {
  552. min-height: 54px;
  553. margin: 0;
  554. font-size: 12.25px;
  555. line-height: 1.7;
  556. color: var(--ai-text-2);
  557. display: -webkit-box;
  558. -webkit-line-clamp: 2;
  559. -webkit-box-orient: vertical;
  560. overflow: hidden;
  561. }
  562. &__chips {
  563. display: flex;
  564. flex-wrap: wrap;
  565. gap: 6px;
  566. }
  567. &__chip {
  568. display: inline-flex;
  569. align-items: center;
  570. gap: 5px;
  571. max-width: 100%;
  572. padding: 1px 8px;
  573. overflow: hidden;
  574. font-size: 11px;
  575. color: var(--ai-text-2);
  576. white-space: nowrap;
  577. text-overflow: ellipsis;
  578. background: var(--ai-bg-subtle);
  579. border-radius: 99px;
  580. }
  581. &__chip-dot {
  582. flex: 0 0 auto;
  583. width: 5px;
  584. height: 5px;
  585. border-radius: 50%;
  586. }
  587. &__card-ops {
  588. display: flex;
  589. align-items: center;
  590. gap: var(--ai-sp-1);
  591. padding-top: 8px;
  592. /* 卡片等高时把操作行压到底部,多张卡片的操作行对齐 */
  593. margin-top: auto;
  594. border-top: 1px solid var(--ai-border);
  595. }
  596. /* 「开始分析」=设计稿的主色文字按钮,行为不变(emit chat) */
  597. &__run {
  598. display: inline-flex;
  599. align-items: center;
  600. gap: 5px;
  601. padding: 4px 8px;
  602. font-size: 12.5px;
  603. font-weight: 600;
  604. font-family: inherit;
  605. color: var(--ai-primary);
  606. background: none;
  607. border: none;
  608. border-radius: 6px;
  609. cursor: pointer;
  610. &:hover {
  611. background: var(--ai-primary-soft);
  612. }
  613. }
  614. &__card-ops-sp {
  615. flex: 1 1 auto;
  616. }
  617. /* 编辑/删除收成图标按钮:与主按钮同排但视觉退后一档,hover 才显形 */
  618. &__iconbtn {
  619. display: inline-flex;
  620. align-items: center;
  621. justify-content: center;
  622. width: 26px;
  623. height: 26px;
  624. padding: 0;
  625. color: var(--ai-text-2);
  626. background: transparent;
  627. border: none;
  628. border-radius: 6px;
  629. cursor: pointer;
  630. transition:
  631. color 0.15s ease,
  632. background 0.15s ease;
  633. &:hover {
  634. color: var(--ai-text);
  635. background: var(--ai-bg-subtle);
  636. }
  637. &.is-danger:hover {
  638. color: @error-color;
  639. background: fade(@error-color, 8%);
  640. }
  641. }
  642. &__empty {
  643. padding: 48px;
  644. font-size: 13px;
  645. color: var(--ai-text-2);
  646. text-align: center;
  647. border: 1px dashed var(--ai-border);
  648. border-radius: 10px;
  649. }
  650. }
  651. /* ── 方案详情弹框(只读):头 + 提示词 + 工具包/技能勾选 ── */
  652. .ae-detail {
  653. display: flex;
  654. flex-direction: column;
  655. gap: var(--ai-sp-3);
  656. &__head {
  657. display: flex;
  658. align-items: center;
  659. gap: var(--ai-sp-2);
  660. }
  661. &__badge {
  662. flex: 0 0 auto;
  663. padding: 0 7px;
  664. font-size: 11px;
  665. line-height: 18px;
  666. border-radius: 99px;
  667. &.is-builtin {
  668. color: var(--ai-text-2);
  669. background: var(--ai-bg-subtle);
  670. }
  671. &.is-custom {
  672. color: var(--ai-primary);
  673. background: var(--ai-primary-soft);
  674. }
  675. }
  676. &__name {
  677. overflow: hidden;
  678. font-size: 15px;
  679. font-weight: 700;
  680. color: var(--ai-text);
  681. white-space: nowrap;
  682. text-overflow: ellipsis;
  683. }
  684. &__desc {
  685. margin: 0;
  686. font-size: 13px;
  687. line-height: 1.7;
  688. color: var(--ai-text-2);
  689. }
  690. &__panel {
  691. display: flex;
  692. flex-direction: column;
  693. gap: 8px;
  694. padding: 12px 14px;
  695. background: var(--ai-bg-card, #fff);
  696. border: 1px solid var(--ai-border);
  697. border-radius: var(--ai-radius-lg);
  698. }
  699. &__label {
  700. font-size: 12.5px;
  701. font-weight: 700;
  702. color: var(--ai-text);
  703. }
  704. /* 提示词可能很长:限高 + 内部滚动,保留换行 */
  705. &__prompt {
  706. overflow-y: auto;
  707. max-height: 240px;
  708. padding: 10px 12px;
  709. margin: 0;
  710. font-family: inherit;
  711. font-size: 12.5px;
  712. line-height: 1.7;
  713. color: var(--ai-text);
  714. white-space: pre-wrap;
  715. word-break: break-word;
  716. background: var(--ai-bg-subtle);
  717. border-radius: var(--ai-radius);
  718. }
  719. &__chips {
  720. display: flex;
  721. flex-wrap: wrap;
  722. gap: 6px;
  723. }
  724. &__chip {
  725. overflow: hidden;
  726. max-width: 260px;
  727. padding: 1px 10px;
  728. font-size: 11.5px;
  729. font-weight: 500;
  730. color: var(--ai-primary);
  731. background: var(--ai-primary-soft);
  732. border-radius: 99px;
  733. white-space: nowrap;
  734. text-overflow: ellipsis;
  735. }
  736. &__none {
  737. font-size: 12.5px;
  738. color: var(--ai-text-2);
  739. }
  740. }
  741. </style>