index.vue 75 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620
  1. <template>
  2. <div class="scheme-container">
  3. <el-card class="box-card">
  4. <template #header>
  5. <div class="card-header">
  6. <div class="header-left">
  7. <span class="title-text">PPT方案管理</span>
  8. <div class="header-filters">
  9. <el-tabs v-model="activeTab" class="premium-status-tabs">
  10. <el-tab-pane label="全部" name="all" />
  11. <el-tab-pane label="我创建的" name="mine" />
  12. <el-tab-pane label="分享给我的" name="sharedToMe" />
  13. </el-tabs>
  14. <el-input v-model="topSearchText" placeholder="搜索方案名称..." :prefix-icon="Search" clearable class="top-search-input" />
  15. </div>
  16. </div>
  17. <div class="header-actions">
  18. <el-button type="danger" plain :disabled="!selectedRows.length" @click="handleBatchDelete">批量删除</el-button>
  19. <el-button type="success" plain :disabled="!selectedRows.length" @click="handleBatchDownload">批量下载</el-button>
  20. <el-button type="primary" @click="openAddScheme">
  21. <el-icon class="mr-5"><Plus /></el-icon>新增PPT方案
  22. </el-button>
  23. </div>
  24. </div>
  25. </template>
  26. <el-table :data="paginatedSchemes" style="width: 100%" @selection-change="handleSelectionChange" v-loading="loading">
  27. <el-table-column type="selection" width="55" />
  28. <el-table-column label="方案基本信息" min-width="260">
  29. <template #default="scope">
  30. <div class="scheme-name-box">
  31. <div class="s-title-row">
  32. <span class="s-name" @click="handleEditScheme(scope.row)">{{ scope.row.name }}</span>
  33. <div class="s-share-line" v-if="scope.row.sharer">
  34. <el-icon><Share /></el-icon>
  35. <span>来自 {{ scope.row.sharer }} 的分享 ({{ scope.row.shareTime }})</span>
  36. </div>
  37. </div>
  38. <div class="s-meta-line">
  39. <span class="label">创建于:</span> {{ scope.row.createTime }}
  40. <span class="divider">|</span>
  41. <span class="label">创建人:</span> {{ scope.row.creator || '管理员' }}
  42. </div>
  43. </div>
  44. </template>
  45. </el-table-column>
  46. <el-table-column label="模板封面" width="120">
  47. <template #default="scope">
  48. <el-image :src="scope.row.template.cover" class="table-thumb" fit="cover" />
  49. </template>
  50. </el-table-column>
  51. <el-table-column label="模板名称" min-width="150" show-overflow-tooltip>
  52. <template #default="scope">
  53. <span>{{ scope.row.template.name }}</span>
  54. </template>
  55. </el-table-column>
  56. <el-table-column label="规格布局" width="160">
  57. <template #default="scope">
  58. <el-tag size="small" :type="getLayoutTagType(scope.row.template.itemsPerPage)">
  59. {{ getLayoutName(scope.row.template.itemsPerPage) }}
  60. </el-tag>
  61. </template>
  62. </el-table-column>
  63. <el-table-column label="商品数量" width="100" align="center">
  64. <template #default="scope">
  65. {{ scope.row.products.length }}
  66. </template>
  67. </el-table-column>
  68. <el-table-column label="操作" width="440" fixed="right">
  69. <template #default="scope">
  70. <el-button link type="primary" @click="handleDownload(scope.row)">下载</el-button>
  71. <el-button link type="primary" @click="handleExportProducts(scope.row)">导出商品</el-button>
  72. <el-button link type="primary" @click="handlePreview(scope.row)">预览</el-button>
  73. <el-button link type="success" @click="openShareDrawer(scope.row)">分享</el-button>
  74. <el-divider direction="vertical" />
  75. <el-button link type="warning" @click="handleEditAtStep(scope.row, 1)">重选商品</el-button>
  76. <el-button link type="warning" @click="handleEditAtStep(scope.row, 0)">编辑模板</el-button>
  77. <el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
  78. </template>
  79. </el-table-column>
  80. </el-table>
  81. <div class="pagination-box">
  82. <el-pagination
  83. v-model:current-page="currentPage"
  84. v-model:page-size="pageSize"
  85. :page-sizes="[10, 20, 50]"
  86. layout="total, sizes, prev, pager, next, jumper"
  87. :total="filteredSchemes.length"
  88. />
  89. </div>
  90. </el-card>
  91. <!-- 主抽屉:新增/编辑方案 wizard -->
  92. <el-drawer
  93. v-model="schemeDrawerVisible"
  94. :title="isEdit ? '修改PPT方案内容' : '创建全新PPT方案'"
  95. direction="rtl"
  96. size="50%"
  97. custom-class="fancy-scheme-drawer"
  98. >
  99. <div class="wizard-container">
  100. <el-steps :active="activeStep" finish-status="success" align-center class="step-bar">
  101. <el-step title="配置模板" />
  102. <el-step title="选取商品" />
  103. <el-step title="预览生成" />
  104. </el-steps>
  105. <div class="step-content">
  106. <!-- Step 1: 模板配置 -->
  107. <div v-if="activeStep === 0" class="step-pane fade-in">
  108. <div class="scheme-base-form">
  109. <el-form label-position="top">
  110. <el-form-item label="本次方案名称" required>
  111. <el-input v-model="currentScheme.name" placeholder="请输入方案名称,如:2024春季选礼方案" />
  112. </el-form-item>
  113. </el-form>
  114. </div>
  115. <div class="tpl-selector-area">
  116. <div class="area-header">
  117. <h4 class="sub-title">1. 选择并配置PPT视觉模板</h4>
  118. <el-button type="primary" size="default" @click="openTplSelector">从模板库更换底版</el-button>
  119. </div>
  120. <!-- 选中的模板快照与实时配置 -->
  121. <div class="tpl-config-layout">
  122. <div class="tpl-preview-side">
  123. <div class="tpl-snap">
  124. <el-image v-if="currentScheme.template.cover" :src="currentScheme.template.cover" fit="cover" class="tpl-snap-img" />
  125. <div class="tpl-snap-badge">{{ getLayoutName(currentScheme.template.itemsPerPage) }}</div>
  126. </div>
  127. <div class="tpl-base-info">
  128. <div class="name">{{ currentScheme.template.name }}</div>
  129. <div class="desc">
  130. 主题色:
  131. <span class="color-dot" :style="{ backgroundColor: currentScheme.template.themeColor }"></span>
  132. <span class="color-val-text">{{ currentScheme.template.themeColor }}</span>
  133. </div>
  134. </div>
  135. </div>
  136. <div class="tpl-form-side">
  137. <el-form :model="currentScheme.template" label-position="top" size="default">
  138. <div class="form-group-title">首页文字 (Cover Page)</div>
  139. <el-row :gutter="15">
  140. <el-col :span="12">
  141. <el-form-item label="封面主标题">
  142. <el-input v-model="currentScheme.template.title" placeholder="PPT首页醒目大标题" />
  143. </el-form-item>
  144. </el-col>
  145. <el-col :span="12">
  146. <el-form-item label="封面副标题">
  147. <el-input v-model="currentScheme.template.subTitle" placeholder="辅助说明文案" />
  148. </el-form-item>
  149. </el-col>
  150. </el-row>
  151. <el-form-item label="PPT封面底图">
  152. <el-upload action="#" :auto-upload="false" :show-file-list="false" :on-change="(file) => handleStaticFileChange(file, 'cover')">
  153. <div class="banner-upload-box" v-if="currentScheme.template.cover">
  154. <img :src="currentScheme.template.cover" />
  155. <div class="mask">重选背景</div>
  156. </div>
  157. <el-button v-else size="small" type="primary" plain :loading="uploadingProp === 'cover'">
  158. {{ uploadingProp === 'cover' ? '上传中...' : '上传封面底图' }}
  159. </el-button>
  160. </el-upload>
  161. </el-form-item>
  162. <el-form-item label="方案截止日期">
  163. <el-date-picker
  164. v-model="currentScheme.template.validity"
  165. type="date"
  166. format="YYYY年MM月DD日"
  167. value-format="方案有效期:YYYY年MM月DD日"
  168. style="width: 100%"
  169. />
  170. </el-form-item>
  171. <div class="form-group-title">品牌与联系信息 (Footer Bar)</div>
  172. <el-row :gutter="15">
  173. <el-col :span="8">
  174. <el-form-item label="底部品牌LOGO">
  175. <el-upload
  176. action="#"
  177. :auto-upload="false"
  178. :show-file-list="false"
  179. :on-change="(file) => handleStaticFileChange(file, 'coverLogo')"
  180. >
  181. <div class="mini-upload-box" v-if="currentScheme.template.coverLogo">
  182. <img :src="currentScheme.template.coverLogo" />
  183. </div>
  184. <el-button v-else size="small" :loading="uploadingProp === 'coverLogo'">
  185. {{ uploadingProp === 'coverLogo' ? '上传中...' : '上传Logo' }}
  186. </el-button>
  187. </el-upload>
  188. </el-form-item>
  189. </el-col>
  190. <el-col :span="16">
  191. <el-form-item label="品牌名称">
  192. <el-input v-model="currentScheme.template.brandName" />
  193. </el-form-item>
  194. </el-col>
  195. </el-row>
  196. <el-form-item label="品牌口号 (Slogan)">
  197. <el-input v-model="currentScheme.template.brandSlogan" size="small" />
  198. </el-form-item>
  199. <el-row :gutter="15">
  200. <el-col :span="12">
  201. <el-form-item label="网址">
  202. <el-input v-model="currentScheme.template.website" size="small" />
  203. </el-form-item>
  204. </el-col>
  205. <el-col :span="12">
  206. <el-form-item label="电话">
  207. <el-input v-model="currentScheme.template.phone" size="small" />
  208. </el-form-item>
  209. </el-col>
  210. </el-row>
  211. <div class="form-group-title">内容页及辅助 (Content & Layout)</div>
  212. <el-row :gutter="15">
  213. <el-col :span="16">
  214. <el-form-item label="内页分类标题">
  215. <el-input v-model="currentScheme.template.contentTitle" />
  216. </el-form-item>
  217. </el-col>
  218. <el-col :span="8">
  219. <el-form-item label="内页LOGO">
  220. <el-upload
  221. action="#"
  222. :auto-upload="false"
  223. :show-file-list="false"
  224. :on-change="(file) => handleStaticFileChange(file, 'contentLogo')"
  225. >
  226. <div class="mini-upload-box small" v-if="currentScheme.template.contentLogo">
  227. <img :src="currentScheme.template.contentLogo" />
  228. </div>
  229. <el-button v-else size="small" :loading="uploadingProp === 'contentLogo'">
  230. {{ uploadingProp === 'contentLogo' ? '上传中...' : '上传内页Logo' }}
  231. </el-button>
  232. </el-upload>
  233. </el-form-item>
  234. </el-col>
  235. </el-row>
  236. <el-row :gutter="15" align="middle">
  237. <el-col :span="10">
  238. <el-form-item label="整篇主题色">
  239. <div class="color-picker-group">
  240. <el-color-picker v-model="currentScheme.template.themeColor" />
  241. <span class="color-text">{{ currentScheme.template.themeColor }}</span>
  242. </div>
  243. </el-form-item>
  244. </el-col>
  245. <el-col :span="14">
  246. <el-form-item label="商品排版模式">
  247. <el-radio-group v-model="currentScheme.template.itemsPerPage" size="small">
  248. <el-radio-button :label="1">简约</el-radio-button>
  249. <el-radio-button :label="3">明细</el-radio-button>
  250. <el-radio-button :label="2">交错</el-radio-button>
  251. </el-radio-group>
  252. </el-form-item>
  253. </el-col>
  254. </el-row>
  255. </el-form>
  256. <div class="tpl-tip">* 此处的修改仅对本次方案生效,不会影响模板库中的原始模板。</div>
  257. </div>
  258. </div>
  259. </div>
  260. </div>
  261. <!-- Step 2: 商品管理 -->
  262. <div v-if="activeStep === 1" class="step-pane fade-in flex-col-container">
  263. <div class="prod-selector-area">
  264. <div class="area-header">
  265. <div class="header-left-group">
  266. <h4 class="sub-title">2. 已添加的商品 ({{ currentScheme.products.length }})</h4>
  267. <div class="selected-batch-actions">
  268. <el-input v-model="selectedSearchKeyword" placeholder="在已选中快速定位..." clearable class="premium-search">
  269. <template #prefix>
  270. <el-icon><Search /></el-icon>
  271. </template>
  272. </el-input>
  273. <el-button
  274. type="danger"
  275. plain
  276. :disabled="!selectedRowsInSelectedList.length"
  277. @click="handleSelectedBatchRemove"
  278. class="batch-remove-btn"
  279. >
  280. 批量移除 (已选 {{ selectedRowsInSelectedList.length }})
  281. </el-button>
  282. </div>
  283. </div>
  284. <el-button type="primary" @click="openProductBatchPicker">
  285. <el-icon class="mr-5"><Plus /></el-icon>批量添加商品
  286. </el-button>
  287. </div>
  288. <div class="selected-prods-list" v-if="currentScheme.products.length > 0">
  289. <el-table
  290. :data="paginatedSelectedProds"
  291. height="calc(100vh - 420px)"
  292. class="selected-table-stretch"
  293. @selection-change="handleSelectedRowsChange"
  294. >
  295. <el-table-column type="selection" width="55" />
  296. <el-table-column prop="productNo" label="产品编号" width="120" />
  297. <el-table-column label="商品图" width="90">
  298. <template #default="scope">
  299. <el-image :src="scope.row.productImage" class="picker-prod-thumb" fit="cover" />
  300. </template>
  301. </el-table-column>
  302. <el-table-column prop="itemName" label="商品名称" min-width="180" show-overflow-tooltip />
  303. <el-table-column prop="brandName" label="品牌" width="100" />
  304. <el-table-column label="所属分类" width="150" show-overflow-tooltip>
  305. <template #default="scope">
  306. <el-tag size="small" type="info" plain>{{ scope.row.topCategoryName+'-'+scope.row.mediumCategoryName+'-'+ scope.row.bottomCategoryName}}</el-tag>
  307. </template>
  308. </el-table-column>
  309. <el-table-column prop="specificationsCode" label="型号/规格" width="120" show-overflow-tooltip />
  310. <el-table-column label="价格对比" width="140">
  311. <template #default="scope">
  312. <div class="price-compare">
  313. <div class="m-price">市场价: ¥{{ scope.row.marketPrice }}</div>
  314. <div class="p-price">官网价: ¥{{ scope.row.memberPrice }}</div>
  315. </div>
  316. </template>
  317. </el-table-column>
  318. <el-table-column label="操作" width="80" align="center" fixed="right">
  319. <template #default="scope">
  320. <el-button link type="danger" @click="removeProductFromScheme(currentScheme.products.indexOf(scope.row))">移除</el-button>
  321. </template>
  322. </el-table-column>
  323. </el-table>
  324. <div class="selected-pagination-box">
  325. <el-pagination
  326. v-model:current-page="selectedProdPage"
  327. v-model:page-size="selectedProdPageSize"
  328. :total="selectedFilteredTotal"
  329. :page-sizes="[5, 10, 20, 50]"
  330. layout="total, sizes, prev, pager, next, jumper"
  331. class="premium-pagination"
  332. />
  333. </div>
  334. </div>
  335. <el-empty v-else description="尚未添加商品,请点击上方按钮选取" />
  336. </div>
  337. </div>
  338. <!-- Step 3: 预览与生成 -->
  339. <div v-if="activeStep === 2" class="step-pane fade-in">
  340. <div class="final-summary">
  341. <div class="summary-card">
  342. <div class="sum-icon">✅</div>
  343. <div class="sum-text">
  344. <h3>准备就绪!</h3>
  345. <p>
  346. 方案名称:<strong>{{ currentScheme.name }}</strong>
  347. </p>
  348. <p>
  349. 包含商品:<strong>{{ currentScheme.products.length }} 款</strong>
  350. </p>
  351. <p>
  352. 当前模板:<strong>{{ currentScheme.template.name }}</strong>
  353. </p>
  354. </div>
  355. </div>
  356. <div class="preview-banner-hint">温馨提示:您可以点击下方“预览PPT效果”进行最后的排版确认,如果不满意可以返回上一步。</div>
  357. <div class="action-buttons-group">
  358. <el-button type="info" size="large" @click="previewInWizard">
  359. <el-icon class="mr-5"><Monitor /></el-icon>预览PPT效果
  360. </el-button>
  361. <el-button type="success" size="large" @click="saveAndGenerate(true)">
  362. <el-icon class="mr-5"><Document /></el-icon>生成并下载PPT方案
  363. </el-button>
  364. <el-button type="primary" size="large" @click="saveAndGenerate(false)">
  365. <el-icon class="mr-5"><Flag /></el-icon>完成并仅存为方案
  366. </el-button>
  367. </div>
  368. </div>
  369. </div>
  370. </div>
  371. <div class="wizard-footer">
  372. <div class="footer-center">
  373. <el-button v-if="activeStep > 0" size="large" @click="activeStep--">上一步:返回</el-button>
  374. <el-button v-if="activeStep === 0" type="primary" size="large" @click="activeStep = 1" :disabled="!currentScheme.name"
  375. >下一步:选择商品</el-button
  376. >
  377. <el-button v-if="activeStep === 1" type="primary" size="large" @click="activeStep = 2" :disabled="!currentScheme.products.length"
  378. >下一步:预览生成</el-button
  379. >
  380. <el-button v-if="activeStep === 2" type="primary" plain size="large" @click="activeStep = 0">返回第一步重新配置</el-button>
  381. </div>
  382. </div>
  383. </div>
  384. </el-drawer>
  385. <!-- 子抽屉1:模板库选择器 -->
  386. <el-drawer v-model="tplPickerVisible" title="从模板库中选取" direction="rtl" size="45%" append-to-body>
  387. <div v-loading="tplLoading" class="tpl-grid">
  388. <el-empty v-if="!tplLoading && allTemplates.length === 0" description="暂无可用模板" />
  389. <div
  390. v-for="tpl in allTemplates"
  391. :key="tpl.id"
  392. class="tpl-grid-item"
  393. :class="{ active: currentScheme.template.id === tpl.id }"
  394. @click="selectTemplate(tpl)"
  395. >
  396. <el-image :src="tpl.cover" fit="cover" class="grid-img" />
  397. <div class="grid-name">{{ tpl.name }}</div>
  398. <div class="grid-layout">{{ getLayoutName(tpl.itemsPerPage) }}</div>
  399. </div>
  400. </div>
  401. </el-drawer>
  402. <!-- 【重构】高级商品库批量选取对话框 -->
  403. <el-dialog
  404. v-model="prodPickerVisible"
  405. title="从商品库批量加入 (支持高阶多重筛选)"
  406. width="1100px"
  407. class="premium-picker-dialog"
  408. append-to-body
  409. align-center
  410. >
  411. <div class="prod-picker-box fixed-height">
  412. <!-- 头部搜索/筛选栏 -->
  413. <div class="picker-filter-bar">
  414. <el-input
  415. v-model="searchProd"
  416. placeholder="搜索商品名称"
  417. clearable
  418. class="filter-item search-input"
  419. @keyup.enter="handlePickerSearch"
  420. />
  421. <el-input
  422. v-model="searchProductNo"
  423. placeholder="搜索商品编号"
  424. clearable
  425. class="filter-item search-input"
  426. @keyup.enter="handlePickerSearch"
  427. >
  428. <template #append>
  429. <el-button @click="handlePickerSearch">
  430. <el-icon><Search /></el-icon>
  431. </el-button>
  432. </template>
  433. </el-input>
  434. </div>
  435. <!-- 数据表格 -->
  436. <el-table
  437. ref="pickerTableRef"
  438. :data="pickerProducts"
  439. v-loading="prodLoading"
  440. @selection-change="handleProdSelection"
  441. height="380px"
  442. row-key="id"
  443. class="picker-table"
  444. >
  445. <el-table-column type="selection" width="55" :reserve-selection="true" />
  446. <el-table-column label="商品图" width="90">
  447. <template #default="scope">
  448. <el-image :src="scope.row.productImage" fit="cover" class="picker-prod-thumb" />
  449. </template>
  450. </el-table-column>
  451. <el-table-column prop="productNo" label="产品编号" width="120" />
  452. <el-table-column prop="itemName" label="商品名称" min-width="180" show-overflow-tooltip />
  453. <el-table-column prop="brandName" label="品牌" width="100" />
  454. <el-table-column label="所属分类" width="150" show-overflow-tooltip>
  455. <template #default="scope">
  456. <el-tag size="small" type="info" plain>{{ scope.row.topCategoryName+'-'+scope.row.mediumCategoryName+'-'+ scope.row.bottomCategoryName}}</el-tag>
  457. </template>
  458. </el-table-column>
  459. <el-table-column prop="specificationsCode" label="型号/规格" width="120" show-overflow-tooltip />
  460. <el-table-column label="价格对比" width="140">
  461. <template #default="scope">
  462. <div class="price-compare">
  463. <div class="m-price">市场价: ¥{{ scope.row.marketPrice }}</div>
  464. <div class="p-price">官网价: ¥{{ scope.row.memberPrice }}</div>
  465. </div>
  466. </template>
  467. </el-table-column>
  468. </el-table>
  469. </div>
  470. <template #footer>
  471. <!-- 游标分页 -->
  472. <pagination
  473. v-show="pickerProducts.length > 0"
  474. v-model:page="pickerPage"
  475. v-model:limit="pickerPageSize"
  476. v-model:way="pickerWay"
  477. :cursor-mode="true"
  478. :has-more="pickerHasMore"
  479. :auto-scroll="false"
  480. @pagination="fetchPickerProducts"
  481. />
  482. <div class="picker-footer-actions">
  483. <el-button @click="prodPickerVisible = false" size="default">取 消</el-button>
  484. <el-button type="primary" size="default" @click="confirmProducts"> 确认加入方案 (已选 {{ tempSelectedProds.length }} 项) </el-button>
  485. </div>
  486. </template>
  487. </el-dialog>
  488. <!-- 分享抽屉 (右侧弹出) -->
  489. <el-drawer v-model="shareDrawerVisible" title="分享PPT方案给同事" direction="rtl" size="600px" custom-class="share-panel-drawer">
  490. <div class="share-drawer-body">
  491. <div class="share-search-section">
  492. <el-input
  493. v-model="shareSearchText"
  494. placeholder="搜索同事姓名"
  495. :prefix-icon="Search"
  496. clearable
  497. class="premium-search-input"
  498. />
  499. </div>
  500. <div class="share-main-content">
  501. <div class="selection-container">
  502. <h4 class="section-title">选择人员 ({{ selectedEmployees.length }})</h4>
  503. <div class="employee-list-scroll">
  504. <div
  505. v-for="emp in filteredEmployees"
  506. :key="emp.id"
  507. class="emp-item"
  508. :class="{ 'is-selected': selectedEmployees.some((s) => s.id === emp.id) }"
  509. @click="toggleEmployeeSelection(emp)"
  510. >
  511. <el-avatar :size="32" class="emp-avatar">
  512. <el-icon><UserFilled /></el-icon>
  513. </el-avatar>
  514. <div class="emp-info">
  515. <div class="e-name">{{ emp.name }}</div>
  516. <div class="e-dept">{{ emp.dept }}</div>
  517. </div>
  518. <el-icon class="check-icon" v-if="selectedEmployees.some((s) => s.id === emp.id)"><Check /></el-icon>
  519. </div>
  520. </div>
  521. </div>
  522. <div class="selected-summary" v-if="selectedEmployees.length > 0">
  523. <div class="summary-header">已选择:</div>
  524. <div class="summary-tags">
  525. <el-tag v-for="emp in selectedEmployees" :key="emp.id" closable @close="removeEmployee(emp)" class="m-2">
  526. {{ emp.name }}
  527. </el-tag>
  528. </div>
  529. </div>
  530. </div>
  531. </div>
  532. <template #footer>
  533. <div class="share-footer">
  534. <el-button @click="shareDrawerVisible = false">取消</el-button>
  535. <el-button type="primary" :disabled="selectedEmployees.length === 0" @click="confirmShare">
  536. 确认分享 ({{ selectedEmployees.length }}人)
  537. </el-button>
  538. </div>
  539. </template>
  540. </el-drawer>
  541. <!-- 超仿真全幻灯片式滑动实时预览控制屏 -->
  542. <el-dialog v-model="previewVisible" title="PPT演示板实时缩略渲染屏" width="900px" top="3vh" append-to-body class="premium-preview-dialog">
  543. <div
  544. v-if="previewData && previewData.template"
  545. class="preview-scroll-wrapper"
  546. :style="{ '--theme-color': previewData.template.themeColor || '#C00000' }"
  547. >
  548. <!-- 封面页 -->
  549. <div class="preview-slide cover-slide">
  550. <div class="cover-top-img-area">
  551. <img v-if="previewData.template.cover" :src="previewData.template.cover" class="preview-full-bg" />
  552. <div class="preview-bg-mask"></div>
  553. <div class="cover-text-group">
  554. <h2 class="c-main-title">{{ previewData.template.title || 'VIP客户生日礼方案' }}</h2>
  555. <p class="c-sub-title">{{ previewData.template.subTitle || '诞生珍礼,非你莫属' }}</p>
  556. <p class="c-validity">{{ previewData.template.validity || '方案有效期:2026年12月31日' }}</p>
  557. </div>
  558. </div>
  559. <div class="cover-bottom-white-bar">
  560. <div class="bar-left">
  561. <img v-if="previewData.template.coverLogo" :src="previewData.template.coverLogo" class="bar-logo" />
  562. <div class="bar-v-line" v-if="previewData.template.coverLogo"></div>
  563. <div class="bar-brand-info">
  564. <div class="b-name">{{ previewData.template.brandName || '优易365' }}</div>
  565. <div class="b-slogan">{{ previewData.template.brandSlogan || '领先的企业级数字化采购及服务品牌' }}</div>
  566. </div>
  567. </div>
  568. <div class="bar-right">
  569. <div class="b-contact">{{ previewData.template.website || 'www.yoe.com' }}</div>
  570. <div class="b-contact">{{ previewData.template.phone || '400-111-0027' }}</div>
  571. </div>
  572. </div>
  573. </div>
  574. <!-- 内容页:根据 itemsPerPage 动态排版 -->
  575. <div class="preview-slide content-slide" v-for="(pageProds, pageIndex) in chunkedPreviewProducts" :key="'page_' + pageIndex">
  576. <div v-if="previewData.template.itemsPerPage === 3" class="detail-top-bar">
  577. <div class="d-left-title"><span class="red-stick"></span>{{ previewData.template.contentTitle || '精品展示专场' }}</div>
  578. <img v-if="previewData.template.contentLogo" :src="previewData.template.contentLogo" class="d-right-logo" />
  579. </div>
  580. <div class="top-bar" v-else>
  581. <img v-if="previewData.template.contentLogo" :src="previewData.template.contentLogo" style="height: 20px; margin-right: 10px" />
  582. <span>{{ previewData.template.contentTitle || '商品展示方案' }}</span>
  583. </div>
  584. <!-- 布局 1 (1P1) -->
  585. <div v-if="previewData.template.itemsPerPage === 1" class="layout-1p1">
  586. <div class="img-box"><img :src="pageProds[0].productImage" /></div>
  587. <div class="info-box">
  588. <h3 class="prod-name">{{ pageProds[0].itemName }}</h3>
  589. <p>产品编号:{{ pageProds[0].productNo || pageProds[0].barCoding }}</p>
  590. <p>规格型号:{{ pageProds[0].specificationsCode }}</p>
  591. <div class="price-tag">供应价: ¥{{ pageProds[0].marketPrice }}</div>
  592. </div>
  593. </div>
  594. <!-- 布局 2 (1P2) -->
  595. <div v-if="previewData.template.itemsPerPage === 2" class="layout-1p2">
  596. <div class="half upper">
  597. <div class="img-box"><img :src="pageProds[0].productImage" /></div>
  598. <div class="info-box txt-left">
  599. <h3 class="prod-name">{{ pageProds[0].itemName }}</h3>
  600. <p>编号: {{ pageProds[0].productNo || pageProds[0].barCoding }} | 规格: {{ pageProds[0].specificationsCode }}</p>
  601. <span class="price-text">¥ {{ pageProds[0].marketPrice }}</span>
  602. </div>
  603. </div>
  604. <div class="divider"></div>
  605. <div class="half lower" v-if="pageProds.length > 1">
  606. <div class="info-box txt-right">
  607. <h3 class="prod-name">{{ pageProds[1].itemName }}</h3>
  608. <p>编号: {{ pageProds[1].productNo || pageProds[1].barCoding }} | 规格: {{ pageProds[1].specificationsCode }}</p>
  609. <span class="price-text">¥ {{ pageProds[1].marketPrice }}</span>
  610. </div>
  611. <div class="img-box"><img :src="pageProds[1].productImage" /></div>
  612. </div>
  613. </div>
  614. <!-- 布局 3 (1P1-详细版) -->
  615. <div v-if="previewData.template.itemsPerPage === 3" class="layout-1p1-detail">
  616. <div class="d-img-box"><img :src="pageProds[0].productImage" /></div>
  617. <div class="d-info-box">
  618. <h3 class="d-prod-name">{{ pageProds[0].itemName }}</h3>
  619. <div class="d-red-line"></div>
  620. <div class="d-base-props">
  621. <p>规格型号:{{ pageProds[0].specificationsCode }}</p>
  622. <p>
  623. <strong>产品编号:{{ pageProds[0].productNo || pageProds[0].barCoding }}</strong>
  624. </p>
  625. <p>
  626. <strong>品牌名称:{{ pageProds[0].brandName || '未知' }}</strong>
  627. </p>
  628. <p>
  629. <strong>最低起订量:{{ pageProds[0].minOrderQuantity || '1' }}</strong>
  630. </p>
  631. <p class="mt-2">
  632. <strong
  633. >市场价:<span class="strikethrough">{{ pageProds[0].marketPrice || pageProds[0].marketPrice }} 元</span></strong
  634. >
  635. </p>
  636. <p class="strong-red">
  637. <strong>供应价:{{ pageProds[0].marketPrice }}元</strong>
  638. </p>
  639. </div>
  640. <div class="d-grid-props">
  641. <div class="d-grid-col">
  642. <h4>产品属性</h4>
  643. <ul class="d-list">
  644. <li v-for="att in (parseAttributesList(pageProds[0].attributesList).length ? parseAttributesList(pageProds[0].attributesList) : ['正品授权', '闪电发货', '优享服务'])" :key="att">{{ att }}</li>
  645. </ul>
  646. </div>
  647. <div class="d-grid-col">
  648. <h4>产品简介</h4>
  649. <ul class="d-list">
  650. <li v-for="feat in pageProds[0].mainLibraryIntro || ['高端大气', '送礼首选', '品质保障']" :key="feat">{{ feat }}</li>
  651. </ul>
  652. </div>
  653. </div>
  654. </div>
  655. </div>
  656. <div class="bottom-deco"></div>
  657. <div class="corner-deco"></div>
  658. </div>
  659. <!-- 结尾页 (极简设计) -->
  660. <div class="preview-slide end-slide">
  661. <div class="end-content-right">
  662. <h1 class="thanks-title" :style="{ color: previewData.template.themeColor || '#C00000' }">THANKS</h1>
  663. <div class="end-brand-line">
  664. <span class="end-b-name">{{ previewData.template.brandName || '优易365' }}</span>
  665. <span class="end-divider">-</span>
  666. <span class="end-b-slogan" :style="{ color: previewData.template.themeColor || '#C00000' }">{{
  667. previewData.template.brandSlogan || '领先的企业级数字化采购及服务品牌'
  668. }}</span>
  669. </div>
  670. <div class="end-website" :style="{ color: previewData.template.themeColor || '#C00000' }">
  671. {{ previewData.template.website || 'www.yoe365.com' }}
  672. </div>
  673. </div>
  674. </div>
  675. </div>
  676. <div style="text-align: center; margin-top: 15px">
  677. <el-button
  678. type="success"
  679. size="large"
  680. @click="handleDownload(previewData)"
  681. style="width: 240px; font-weight: bold; box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3)"
  682. >
  683. 立即下载导出PPT文件
  684. </el-button>
  685. </div>
  686. </el-dialog>
  687. </div>
  688. </template>
  689. <script setup>
  690. import { ref, computed, onMounted, nextTick, watch } from 'vue';
  691. import { ElMessage, ElMessageBox } from 'element-plus';
  692. import { Plus, Edit, Monitor, Flag, Picture, Collection, Operation, Search, Document, Share, UserFilled, Check } from '@element-plus/icons-vue';
  693. import { generatePPT } from '@/utils/pptPlugin';
  694. import { listPptScheme, addPptScheme, updatePptScheme, delPptScheme, sharePptScheme } from '@/api/product/pptScheme';
  695. import { listPptTemplate } from '@/api/product/pptTemplate';
  696. import { listBase } from '@/api/product/base';
  697. import { listUser } from '@/api/system/user';
  698. import { useUserStore } from '@/store/modules/user';
  699. import request, { globalHeaders, download } from '@/utils/request';
  700. // ==================== 数据转换工具函数 ====================
  701. /**
  702. * 后端 PptSchemeVO(扁平 tpl* 字段)→ 前端 scheme 格式(嵌套 template 对象)
  703. * @param {object} vo - 后端返回的VO
  704. * @param {{ name: string, userId: string|number }} currentUser - 当前登录用户信息
  705. */
  706. const voToScheme = (vo, currentUser = {}) => {
  707. const { userId: currentUserId = '' } = currentUser;
  708. // 分享给我的:ownerId 等于当前用户 且 isShared 为 '1'
  709. const isSharedToMe = vo.isShared === '1' && String(vo.ownerId) === String(currentUserId);
  710. // 我创建的:createBy 等于当前用户 userId
  711. const isMine = String(vo.createBy) === String(currentUserId);
  712. return {
  713. id: vo.id,
  714. name: vo.name,
  715. template: {
  716. id: vo.templateId,
  717. name: vo.tplName,
  718. cover: vo.tplCover,
  719. coverLogo: vo.tplCoverLogo,
  720. contentLogo: vo.tplContentLogo,
  721. themeColor: vo.tplThemeColor,
  722. itemsPerPage: vo.tplItemsPerPage,
  723. title: vo.tplCoverTitle,
  724. subTitle: vo.tplCoverSubTitle,
  725. validity: vo.tplValidity,
  726. brandName: vo.tplBrandName,
  727. brandSlogan: vo.tplBrandSlogan,
  728. website: vo.tplWebsite,
  729. phone: vo.tplPhone,
  730. contentTitle: vo.tplContentTitle
  731. },
  732. products: (() => {
  733. try {
  734. return vo.productData ? JSON.parse(vo.productData) : [];
  735. } catch {
  736. return [];
  737. }
  738. })(),
  739. productNum: vo.productNum ?? 0,
  740. createTime: vo.createTime,
  741. creator: vo.createBy,
  742. sharer: vo.sharerName || null,
  743. shareTime: vo.shareTime || null,
  744. type: isSharedToMe ? 'sharedToMe' : isMine ? 'mine' : 'others'
  745. };
  746. };
  747. /**
  748. * 前端 scheme 格式 → 后端 PptSchemeForm(扁平 tpl* 字段)
  749. */
  750. const schemeToForm = (scheme) => ({
  751. id: scheme.id,
  752. name: scheme.name,
  753. templateId: scheme.template?.id,
  754. tplName: scheme.template?.name,
  755. tplCover: scheme.template?.cover,
  756. tplCoverLogo: scheme.template?.coverLogo,
  757. tplContentLogo: scheme.template?.contentLogo,
  758. tplThemeColor: scheme.template?.themeColor,
  759. tplItemsPerPage: scheme.template?.itemsPerPage,
  760. tplCoverTitle: scheme.template?.title,
  761. tplCoverSubTitle: scheme.template?.subTitle,
  762. tplValidity: scheme.template?.validity,
  763. tplBrandName: scheme.template?.brandName,
  764. tplBrandSlogan: scheme.template?.brandSlogan,
  765. tplWebsite: scheme.template?.website,
  766. tplPhone: scheme.template?.phone,
  767. tplContentTitle: scheme.template?.contentTitle,
  768. productIds: (scheme.products ?? []).map((p) => p.id),
  769. productNum: (scheme.products ?? []).length
  770. });
  771. /**
  772. * 模板库 PptTemplateVO → 前端 template 格式
  773. * (模板VO用 coverTitle/coverSubTitle,前端统一用 title/subTitle)
  774. */
  775. const tplVoToTemplate = (tpl) => ({
  776. id: tpl.id,
  777. name: tpl.name,
  778. cover: tpl.cover,
  779. themeColor: tpl.themeColor,
  780. itemsPerPage: tpl.itemsPerPage,
  781. title: tpl.coverTitle,
  782. subTitle: tpl.coverSubTitle,
  783. brandName: tpl.brandName,
  784. brandSlogan: tpl.brandSlogan,
  785. website: tpl.website,
  786. phone: tpl.phone,
  787. coverLogo: tpl.coverLogo,
  788. contentLogo: tpl.contentLogo,
  789. contentTitle: tpl.contentTitle,
  790. validity: ''
  791. });
  792. // 当前登录用户
  793. const userStore = useUserStore();
  794. // 分享功能相关状态
  795. const allUsers = ref([]);
  796. const usersLoading = ref(false);
  797. const shareDrawerVisible = ref(false);
  798. const sharingScheme = ref(null);
  799. const shareSearchText = ref('');
  800. const selectedEmployees = ref([]);
  801. // 搜索过滤后的用户
  802. const filteredEmployees = computed(() => {
  803. if (!shareSearchText.value) return allUsers.value;
  804. const key = shareSearchText.value.toLowerCase();
  805. return allUsers.value.filter((emp) => emp.name.includes(key) || emp.dept.includes(key));
  806. });
  807. // 数据列表
  808. const schemeList = ref([]);
  809. const allTemplates = ref([]);
  810. const loading = ref(false);
  811. const selectedRows = ref([]);
  812. const currentPage = ref(1);
  813. const pageSize = ref(10);
  814. // 状态控制
  815. const schemeDrawerVisible = ref(false);
  816. const activeStep = ref(0);
  817. const isEdit = ref(false);
  818. const tplPickerVisible = ref(false);
  819. const tplLoading = ref(false);
  820. const prodPickerVisible = ref(false);
  821. const previewVisible = ref(false);
  822. const previewData = ref(null);
  823. // 当前正在新增/编辑的方案对象
  824. const currentScheme = ref({
  825. id: '',
  826. name: '',
  827. template: {},
  828. products: [],
  829. createTime: ''
  830. });
  831. // 分页列表
  832. // 方案主列表过滤逻辑(Tab 过滤已交给后端,此处仅做前端名称搜索)
  833. const filteredSchemes = computed(() => {
  834. let list = schemeList.value;
  835. // 方案名称搜索过滤
  836. if (topSearchText.value) {
  837. const key = topSearchText.value.toLowerCase();
  838. list = list.filter((s) => s.name.toLowerCase().includes(key));
  839. }
  840. return list;
  841. });
  842. const paginatedSchemes = computed(() => {
  843. const start = (currentPage.value - 1) * pageSize.value;
  844. const end = currentPage.value * pageSize.value;
  845. return filteredSchemes.value.slice(start, end);
  846. });
  847. // 已选商品分页逻辑
  848. const selectedProdPage = ref(1);
  849. const selectedProdPageSize = ref(10);
  850. const selectedSearchKeyword = ref('');
  851. const selectedRowsInSelectedList = ref([]);
  852. const displayedSelectedList = computed(() => {
  853. let list = currentScheme.value.products;
  854. if (selectedSearchKeyword.value) {
  855. const key = selectedSearchKeyword.value.toLowerCase();
  856. list = list.filter((p) => p.itemName.toLowerCase().includes(key) || (p.productNo && p.productNo.toLowerCase().includes(key)));
  857. }
  858. return list;
  859. });
  860. const selectedFilteredTotal = computed(() => displayedSelectedList.value.length);
  861. const paginatedSelectedProds = computed(() => {
  862. const start = (selectedProdPage.value - 1) * selectedProdPageSize.value;
  863. const end = selectedProdPage.value * selectedProdPageSize.value;
  864. return displayedSelectedList.value.slice(start, end);
  865. });
  866. // 解析 attributesList JSON 字符串,格式:{"1":["3kg","4kg"]},输出 ["1:3kg、4kg"]
  867. const parseAttributesList = (str) => {
  868. if (!str) return [];
  869. try {
  870. const obj = typeof str === 'string' ? JSON.parse(str) : str;
  871. if (typeof obj !== 'object' || Array.isArray(obj)) return [];
  872. return Object.entries(obj).map(([key, vals]) => `${key}:${Array.isArray(vals) ? vals.join('、') : vals}`);
  873. } catch {
  874. return [];
  875. }
  876. };
  877. const chunkedPreviewProducts = computed(() => {
  878. if (!previewData.value || !previewData.value.products) return [];
  879. const result = [];
  880. const chunk = previewData.value.template.itemsPerPage === 3 ? 1 : previewData.value.template.itemsPerPage || 1;
  881. for (let i = 0; i < previewData.value.products.length; i += chunk) {
  882. result.push(previewData.value.products.slice(i, i + chunk));
  883. }
  884. return result;
  885. });
  886. const handleSelectedRowsChange = (val) => {
  887. selectedRowsInSelectedList.value = val;
  888. };
  889. const handleSelectedBatchRemove = () => {
  890. const idsToRemove = selectedRowsInSelectedList.value.map((r) => r.id);
  891. currentScheme.value.products = currentScheme.value.products.filter((p) => !idsToRemove.includes(p.id));
  892. ElMessage.success(`成功移除 ${idsToRemove.length} 个商品`);
  893. selectedRowsInSelectedList.value = [];
  894. };
  895. // 商品库选择器相关状态
  896. const searchProd = ref('');
  897. const searchProductNo = ref('');
  898. const tempSelectedProds = ref([]);
  899. const prodLoading = ref(false);
  900. const pickerPage = ref(1);
  901. const pickerPageSize = ref(10);
  902. const pickerTotal = ref(0);
  903. const pickerProducts = ref([]);
  904. const pickerHasMore = ref(true); // 是否还有更多数据(游标分页)
  905. const pickerWay = ref(undefined); // 翻页方向:0=上一页,1=下一页
  906. const pickerPageHistory = ref([]); // 每页首尾 id 历史,支持双向翻页
  907. const pickerTableRef = ref(null);
  908. // 服务端游标分页加载商品
  909. const fetchPickerProducts = async () => {
  910. prodLoading.value = true;
  911. try {
  912. const currentPageNum = pickerPage.value;
  913. const params = {
  914. pageNum: currentPageNum,
  915. pageSize: pickerPageSize.value,
  916. itemName: searchProd.value || undefined,
  917. productNo: searchProductNo.value || undefined
  918. };
  919. // 第一页不需要游标参数
  920. if (currentPageNum === 1) {
  921. delete params.lastSeenId;
  922. delete params.way;
  923. } else {
  924. if (pickerWay.value === 0) {
  925. // 上一页:使用目标页的 firstId
  926. const targetPageHistory = pickerPageHistory.value[currentPageNum];
  927. if (targetPageHistory) {
  928. params.firstSeenId = targetPageHistory.firstId;
  929. params.way = 0;
  930. }
  931. } else {
  932. // 下一页:使用前一页的 lastId
  933. const prevPageHistory = pickerPageHistory.value[currentPageNum - 1];
  934. if (prevPageHistory) {
  935. params.lastSeenId = prevPageHistory.lastId;
  936. params.way = 1;
  937. }
  938. }
  939. }
  940. const res = await listBase(params);
  941. pickerProducts.value = (res.rows || []).map((p) => ({
  942. ...p,
  943. marketPrice: p.marketPrice,
  944. categoryName: p.categoryName || '未分类'
  945. }));
  946. pickerTotal.value = res.total || 0;
  947. // 判断是否还有更多数据
  948. pickerHasMore.value = pickerProducts.value.length === pickerPageSize.value;
  949. // 记录当前页的首尾 id
  950. if (pickerProducts.value.length > 0) {
  951. const firstItem = pickerProducts.value[0];
  952. const lastItem = pickerProducts.value[pickerProducts.value.length - 1];
  953. if (pickerPageHistory.value.length <= currentPageNum) {
  954. pickerPageHistory.value[currentPageNum] = {
  955. firstId: firstItem.id,
  956. lastId: lastItem.id
  957. };
  958. }
  959. }
  960. } catch (e) {
  961. ElMessage.error('商品列表加载失败');
  962. } finally {
  963. prodLoading.value = false;
  964. }
  965. nextTick(() => {
  966. syncTableSelection();
  967. });
  968. };
  969. const openProductBatchPicker = () => {
  970. // 1. 初始化临时选中列表
  971. tempSelectedProds.value = [...currentScheme.value.products];
  972. // 2. 重置分页、游标历史与搜索
  973. pickerPage.value = 1;
  974. pickerWay.value = undefined;
  975. pickerPageHistory.value = [];
  976. searchProd.value = '';
  977. searchProductNo.value = '';
  978. prodPickerVisible.value = true;
  979. // 3. 加载第一页数据
  980. fetchPickerProducts();
  981. };
  982. const handleProdSelection = (val) => {
  983. tempSelectedProds.value = val;
  984. };
  985. // 提取同步逻辑,供打开弹窗和翻页时调用
  986. const syncTableSelection = () => {
  987. if (!pickerTableRef.value) return;
  988. const selectedIds = tempSelectedProds.value.map((p) => p.id);
  989. // 遍历当前页显示的商品,如果在已选中列表中,则勾选
  990. pickerProducts.value.forEach((row) => {
  991. const isSelected = selectedIds.includes(row.id);
  992. pickerTableRef.value.toggleRowSelection(row, isSelected);
  993. });
  994. };
  995. const confirmProducts = () => {
  996. // 直接覆盖方案商品列表,这样在弹窗中取消勾选也能生效
  997. currentScheme.value.products = [...tempSelectedProds.value];
  998. prodPickerVisible.value = false;
  999. ElMessage.success(`商品清单已更新,当前共 ${currentScheme.value.products.length} 项`);
  1000. };
  1001. const handlePickerSearch = () => {
  1002. pickerPage.value = 1;
  1003. pickerWay.value = undefined;
  1004. pickerPageHistory.value = [];
  1005. fetchPickerProducts();
  1006. };
  1007. // 数据加载与持久化
  1008. // 数据加载与持久化
  1009. const topSearchText = ref('');
  1010. const activeTab = ref('all');
  1011. // 数据加载:根据当前 Tab 传入对应查询参数,由后端过滤
  1012. const loadAllData = async () => {
  1013. loading.value = true;
  1014. try {
  1015. // 根据 Tab 构建查询参数
  1016. const query = {};
  1017. if (activeTab.value === 'mine') {
  1018. // 我创建的:createBy = 当前用户 ID
  1019. query.createBy = userStore.userId;
  1020. } else if (activeTab.value === 'sharedToMe') {
  1021. // 分享给我的:ownerId = 当前用户 ID 且 isShared = '1'
  1022. query.sharerId = userStore.userId;
  1023. query.isShared = '1';
  1024. }
  1025. const schemeRes = await listPptScheme(query);
  1026. const currentUser = { userId: userStore.userId };
  1027. schemeList.value = (schemeRes.rows || []).map((vo) => voToScheme(vo, currentUser));
  1028. } catch (e) {
  1029. ElMessage.error('数据加载失败');
  1030. } finally {
  1031. loading.value = false;
  1032. }
  1033. };
  1034. onMounted(() => {
  1035. loadAllData();
  1036. });
  1037. // 切换 Tab 时重新请求,并重置到第一页
  1038. watch(activeTab, () => {
  1039. currentPage.value = 1;
  1040. loadAllData();
  1041. });
  1042. // 列表常规操作
  1043. const handleSelectionChange = (val) => {
  1044. selectedRows.value = val;
  1045. };
  1046. const getLayoutName = (val) => {
  1047. if (val === 3) return '单品详细明细 (1P3)';
  1048. if (val === 2) return '交错展示 (1P2)';
  1049. return '基础展示 (1P1)';
  1050. };
  1051. const getLayoutTagType = (val) => {
  1052. if (val === 3) return 'danger';
  1053. if (val === 2) return 'warning';
  1054. return '';
  1055. };
  1056. const handleDelete = (id) => {
  1057. ElMessageBox.confirm('确定删除该方案吗?', '提示')
  1058. .then(() => {
  1059. delPptScheme(id).then(() => {
  1060. ElMessage.success('已删除');
  1061. loadAllData();
  1062. });
  1063. })
  1064. .catch(() => {});
  1065. };
  1066. const handleBatchDelete = () => {
  1067. ElMessageBox.confirm(`确定批量删除选中的 ${selectedRows.value.length} 个方案吗?`, '警告').then(() => {
  1068. const ids = selectedRows.value.map((r) => r.id);
  1069. delPptScheme(ids).then(() => {
  1070. ElMessage.success('批量删除成功');
  1071. loadAllData();
  1072. });
  1073. });
  1074. };
  1075. const handleDownload = async (scheme) => {
  1076. try {
  1077. ElMessage.info('PPT渲染及转换中,请稍候...');
  1078. await generatePPT(scheme.template, scheme.products);
  1079. ElMessage.success('PPT下载成功');
  1080. } catch (err) {
  1081. ElMessage.error('渲染PPT失败:' + err.message);
  1082. }
  1083. };
  1084. const handleBatchDownload = async () => {
  1085. ElMessage.warning('批量导出 PPT 可能会有较大浏览器资源占用,请耐心等待文件弹出。');
  1086. for (const r of selectedRows.value) {
  1087. await handleDownload(r);
  1088. }
  1089. };
  1090. const handleExportProducts = (scheme) => {
  1091. download('/product/pptTemplate/exportSchemeProducts', { schemeId: scheme.id }, `${scheme.name || 'PPT方案'}_商品导出.xlsx`);
  1092. };
  1093. const handlePreview = (row) => {
  1094. previewData.value = row;
  1095. previewVisible.value = true;
  1096. };
  1097. // 分享功能逻辑
  1098. const openShareDrawer = async (row) => {
  1099. sharingScheme.value = row;
  1100. selectedEmployees.value = [];
  1101. shareSearchText.value = '';
  1102. shareDrawerVisible.value = true;
  1103. // 首次打开时加载用户列表(之后复用缓存)
  1104. if (!allUsers.value.length) {
  1105. usersLoading.value = true;
  1106. try {
  1107. const res = await listUser({ pageNum: 1, pageSize: 500, userSonType: 0 });
  1108. allUsers.value = (res.rows || []).map((u) => ({
  1109. id: u.userId,
  1110. name: u.nickName,
  1111. dept: u.deptName || '',
  1112. email: u.email || ''
  1113. }));
  1114. } catch (e) {
  1115. // 错误由请求拦截器统一处理
  1116. } finally {
  1117. usersLoading.value = false;
  1118. }
  1119. }
  1120. };
  1121. const toggleEmployeeSelection = (emp) => {
  1122. const idx = selectedEmployees.value.findIndex((s) => s.id === emp.id);
  1123. if (idx > -1) {
  1124. selectedEmployees.value.splice(idx, 1);
  1125. } else {
  1126. selectedEmployees.value.push(emp);
  1127. }
  1128. };
  1129. const removeEmployee = (emp) => {
  1130. selectedEmployees.value = selectedEmployees.value.filter((s) => s.id !== emp.id);
  1131. };
  1132. const confirmShare = async () => {
  1133. if (!sharingScheme.value) return;
  1134. const userIds = selectedEmployees.value.map((e) => e.id);
  1135. const names = selectedEmployees.value.map((e) => e.name).join('、');
  1136. try {
  1137. await sharePptScheme(sharingScheme.value.id, userIds);
  1138. shareDrawerVisible.value = false;
  1139. ElMessage({ type: 'success', message: `分享成功!方案已同步至:${names}`, duration: 5000 });
  1140. await loadAllData();
  1141. } catch (e) {
  1142. // 错误由请求拦截器统一处理
  1143. }
  1144. };
  1145. // Wizard 新增/编辑流程逻辑
  1146. const openAddScheme = async () => {
  1147. isEdit.value = false;
  1148. activeStep.value = 0;
  1149. // 生成今天的截止日期字符串(与 value-format 保持一致)
  1150. const today = new Date();
  1151. const yyyy = today.getFullYear();
  1152. const mm = String(today.getMonth() + 1).padStart(2, '0');
  1153. const dd = String(today.getDate()).padStart(2, '0');
  1154. const todayValidity = `方案有效期:${yyyy}年${mm}月${dd}日`;
  1155. // 按需加载模板列表,取第一个作为默认模板
  1156. let defaultTemplate = { id: 99, name: '默认通用模板', cover: '', themeColor: '#C00000', itemsPerPage: 1, validity: todayValidity };
  1157. try {
  1158. const res = await listPptTemplate();
  1159. const tplList = (res.rows || []).map(tplVoToTemplate);
  1160. if (tplList.length) {
  1161. allTemplates.value = tplList;
  1162. defaultTemplate = { ...tplList[0], validity: todayValidity };
  1163. } else if (!allTemplates.value.length) {
  1164. allTemplates.value = [defaultTemplate];
  1165. }
  1166. } catch (e) {
  1167. if (!allTemplates.value.length) {
  1168. allTemplates.value = [defaultTemplate];
  1169. }
  1170. }
  1171. currentScheme.value = {
  1172. id: Date.now(),
  1173. name: '新建PPT方案_' + new Date().toLocaleDateString(),
  1174. template: { ...defaultTemplate },
  1175. products: [],
  1176. createTime: new Date().toLocaleString(),
  1177. creator: '管理员'
  1178. };
  1179. schemeDrawerVisible.value = true;
  1180. };
  1181. const handleEditScheme = (row) => {
  1182. isEdit.value = true;
  1183. activeStep.value = 0;
  1184. currentScheme.value = JSON.parse(JSON.stringify(row));
  1185. schemeDrawerVisible.value = true;
  1186. };
  1187. const handleEditAtStep = (row, step = 0) => {
  1188. isEdit.value = true;
  1189. activeStep.value = step;
  1190. currentScheme.value = JSON.parse(JSON.stringify(row));
  1191. schemeDrawerVisible.value = true;
  1192. };
  1193. const openTplSelector = async () => {
  1194. tplPickerVisible.value = true;
  1195. tplLoading.value = true;
  1196. try {
  1197. const res = await listPptTemplate();
  1198. const tplList = (res.rows || []).map(tplVoToTemplate);
  1199. allTemplates.value = tplList.length ? tplList : [{ id: 99, name: '默认通用模板', cover: '', themeColor: '#C00000', itemsPerPage: 1 }];
  1200. } catch (e) {
  1201. ElMessage.error('模板列表加载失败');
  1202. } finally {
  1203. tplLoading.value = false;
  1204. }
  1205. };
  1206. const selectTemplate = (tpl) => {
  1207. currentScheme.value.template = { ...tpl };
  1208. tplPickerVisible.value = false;
  1209. };
  1210. const uploadingProp = ref(''); // 正在上传的字段名,用于显示加载状态
  1211. const handleStaticFileChange = async (file, prop) => {
  1212. if (!file || !file.raw) return;
  1213. uploadingProp.value = prop;
  1214. try {
  1215. const formData = new FormData();
  1216. formData.append('file', file.raw);
  1217. const res = await request({
  1218. url: '/resource/oss/upload',
  1219. method: 'post',
  1220. headers: { 'Content-Type': 'multipart/form-data' },
  1221. data: formData
  1222. });
  1223. if (res.data && res.data.url) {
  1224. currentScheme.value.template[prop] = res.data.url;
  1225. } else {
  1226. ElMessage.error('图片上传失败');
  1227. }
  1228. } catch (e) {
  1229. ElMessage.error('图片上传失败,请重试');
  1230. } finally {
  1231. uploadingProp.value = '';
  1232. }
  1233. };
  1234. const removeProductFromScheme = (idx) => {
  1235. currentScheme.value.products.splice(idx, 1);
  1236. };
  1237. const previewInWizard = () => {
  1238. previewData.value = currentScheme.value;
  1239. previewVisible.value = true;
  1240. };
  1241. const saveAndGenerate = async (download = false) => {
  1242. const formData = schemeToForm(currentScheme.value);
  1243. try {
  1244. if (isEdit.value) {
  1245. await updatePptScheme(formData);
  1246. } else {
  1247. await addPptScheme(formData);
  1248. }
  1249. schemeDrawerVisible.value = false;
  1250. await loadAllData();
  1251. if (download) {
  1252. await handleDownload(currentScheme.value);
  1253. } else {
  1254. ElMessage.success('方案已保存至列表');
  1255. }
  1256. } catch (e) {
  1257. ElMessage.error('保存失败,请重试');
  1258. }
  1259. };
  1260. </script>
  1261. <style scoped>
  1262. .ml-10 {
  1263. margin-left: 10px;
  1264. }
  1265. .mr-5 {
  1266. margin-right: 5px;
  1267. }
  1268. .mb-10 {
  1269. margin-bottom: 10px;
  1270. }
  1271. .header-left {
  1272. display: flex;
  1273. align-items: center;
  1274. gap: 40px;
  1275. flex: 1;
  1276. }
  1277. .header-filters {
  1278. display: flex;
  1279. align-items: center;
  1280. gap: 35px;
  1281. flex: 1;
  1282. }
  1283. .premium-status-tabs {
  1284. margin-bottom: -2px;
  1285. }
  1286. .premium-status-tabs :deep(.el-tabs__header) {
  1287. margin-bottom: 0;
  1288. border-bottom: none;
  1289. }
  1290. .premium-status-tabs :deep(.el-tabs__nav-wrap::after) {
  1291. height: 0;
  1292. }
  1293. .premium-status-tabs :deep(.el-tabs__item) {
  1294. font-size: 16px;
  1295. font-weight: 500;
  1296. color: #606266;
  1297. padding: 0 25px;
  1298. height: 50px;
  1299. line-height: 50px;
  1300. transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  1301. }
  1302. .premium-status-tabs :deep(.el-tabs__item.is-active) {
  1303. color: #409eff;
  1304. font-weight: bold;
  1305. font-size: 17px;
  1306. }
  1307. .premium-status-tabs :deep(.el-tabs__active-bar) {
  1308. height: 3px;
  1309. border-radius: 3px;
  1310. background-color: #409eff;
  1311. }
  1312. .top-search-input {
  1313. width: 350px;
  1314. }
  1315. .top-search-input :deep(.el-input__wrapper) {
  1316. border-radius: 12px;
  1317. background: #f5f7fa;
  1318. box-shadow: none !important;
  1319. border: 1px solid #e4e7ed;
  1320. height: 40px;
  1321. transition: all 0.3s;
  1322. }
  1323. .top-search-input :deep(.el-input__wrapper.is-focus) {
  1324. background: #fff;
  1325. border-color: #409eff;
  1326. }
  1327. .card-header {
  1328. display: flex;
  1329. justify-content: space-between;
  1330. align-items: center;
  1331. width: 100%;
  1332. }
  1333. .header-left .title-text {
  1334. font-size: 18px;
  1335. font-weight: 600;
  1336. color: #303133;
  1337. }
  1338. .scheme-name-box .s-name {
  1339. font-weight: 600;
  1340. color: #409eff;
  1341. font-size: 15px;
  1342. }
  1343. .scheme-name-box .s-time {
  1344. font-size: 12px;
  1345. color: #999;
  1346. margin-top: 4px;
  1347. }
  1348. .table-thumb {
  1349. width: 80px;
  1350. height: 50px;
  1351. border-radius: 4px;
  1352. border: 1px solid #eee;
  1353. }
  1354. .tpl-info .tpl-name {
  1355. font-size: 13px;
  1356. color: #666;
  1357. margin-bottom: 4px;
  1358. }
  1359. .pagination-box {
  1360. margin-top: 20px;
  1361. display: flex;
  1362. justify-content: flex-end;
  1363. }
  1364. /* Wizard Styles */
  1365. .wizard-container {
  1366. height: 100%;
  1367. display: flex;
  1368. flex-direction: column;
  1369. }
  1370. .step-bar {
  1371. padding: 10px 0;
  1372. border-bottom: 1px solid #f0f2f5;
  1373. }
  1374. .step-content {
  1375. flex: 1;
  1376. padding: 15px 20px;
  1377. overflow-y: auto;
  1378. overflow-x: hidden;
  1379. background: #fff;
  1380. }
  1381. .flex-col-container {
  1382. display: flex;
  1383. flex-direction: column;
  1384. height: 100%;
  1385. overflow: hidden;
  1386. }
  1387. .prod-selector-area {
  1388. flex: 1;
  1389. display: flex;
  1390. flex-direction: column;
  1391. overflow: hidden;
  1392. }
  1393. .selected-prods-list {
  1394. flex: 1;
  1395. display: flex;
  1396. flex-direction: column;
  1397. overflow: hidden;
  1398. }
  1399. .wizard-footer {
  1400. height: 70px;
  1401. padding: 0 20px;
  1402. border-top: 1px solid #f0f2f5;
  1403. display: flex;
  1404. align-items: center;
  1405. justify-content: center;
  1406. background: #fff;
  1407. }
  1408. .footer-center {
  1409. display: flex;
  1410. gap: 20px;
  1411. align-items: center;
  1412. justify-content: center;
  1413. }
  1414. .selected-table-stretch {
  1415. border: 1px solid #ebeef5;
  1416. border-radius: 8px;
  1417. overflow: hidden;
  1418. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
  1419. }
  1420. .selected-pagination-box {
  1421. margin-top: 20px;
  1422. padding: 15px;
  1423. background: #fcfcfc;
  1424. border-radius: 8px;
  1425. border: 1px solid #f0f0f0;
  1426. display: flex;
  1427. justify-content: flex-end;
  1428. box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
  1429. }
  1430. .dialog-pagination-fix {
  1431. margin-top: 15px;
  1432. }
  1433. /* Premium Search Style */
  1434. .premium-search {
  1435. width: 260px;
  1436. }
  1437. .premium-search :deep(.el-input__wrapper) {
  1438. border-radius: 20px;
  1439. background-color: #f5f7fa;
  1440. border: 1px solid #e4e7ed;
  1441. box-shadow: none !important;
  1442. padding-left: 15px;
  1443. transition: all 0.3s;
  1444. }
  1445. .premium-search :deep(.el-input__wrapper.is-focus) {
  1446. background-color: #fff;
  1447. border-color: #409eff;
  1448. box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1) !important;
  1449. }
  1450. .batch-remove-btn {
  1451. border-radius: 20px;
  1452. padding: 0 20px;
  1453. }
  1454. /* Premium Pagination Styles */
  1455. .premium-pagination :deep(.el-pagination__total) {
  1456. font-weight: 600;
  1457. color: #606266;
  1458. }
  1459. .premium-pagination :deep(.el-pager li) {
  1460. border-radius: 4px;
  1461. margin: 0 3px;
  1462. border: 1px solid #dcdfe6;
  1463. transition: all 0.3s;
  1464. }
  1465. .premium-pagination :deep(.el-pager li.is-active) {
  1466. background-color: #409eff;
  1467. color: #fff;
  1468. border-color: #409eff;
  1469. box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
  1470. }
  1471. .premium-pagination :deep(.btn-prev),
  1472. .premium-pagination :deep(.btn-next) {
  1473. border-radius: 4px;
  1474. border: 1px solid #dcdfe6;
  1475. }
  1476. .header-left-group {
  1477. display: flex;
  1478. align-items: center;
  1479. gap: 30px;
  1480. }
  1481. .selected-batch-actions {
  1482. display: flex;
  1483. align-items: center;
  1484. gap: 10px;
  1485. }
  1486. .mini-search-in-selected {
  1487. width: 220px;
  1488. }
  1489. /* 滚动条美化 */
  1490. ::-webkit-scrollbar {
  1491. width: 6px;
  1492. height: 6px;
  1493. }
  1494. ::-webkit-scrollbar-thumb {
  1495. background: #ccc;
  1496. border-radius: 10px;
  1497. }
  1498. ::-webkit-scrollbar-thumb:hover {
  1499. background: #999;
  1500. }
  1501. ::-webkit-scrollbar-track {
  1502. background: #f1f1f1;
  1503. border-radius: 10px;
  1504. }
  1505. .sub-title {
  1506. font-size: 15px;
  1507. font-weight: 600;
  1508. color: #555;
  1509. margin: 0;
  1510. }
  1511. .area-header {
  1512. display: flex;
  1513. justify-content: space-between;
  1514. align-items: center;
  1515. margin-bottom: 15px;
  1516. margin-top: 10px;
  1517. }
  1518. .tpl-snap-img {
  1519. width: 140px;
  1520. height: 80px;
  1521. border-radius: 6px;
  1522. border: 1px solid #ddd;
  1523. }
  1524. .tpl-config-layout {
  1525. display: flex;
  1526. gap: 30px;
  1527. margin-top: 10px;
  1528. }
  1529. .tpl-preview-side {
  1530. width: 180px;
  1531. flex-shrink: 0;
  1532. }
  1533. .tpl-form-side {
  1534. flex: 1;
  1535. background: #fff;
  1536. border: 1px solid #eee;
  1537. padding: 20px;
  1538. border-radius: 12px;
  1539. box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05);
  1540. }
  1541. .tpl-snap {
  1542. position: relative;
  1543. }
  1544. .tpl-snap-badge {
  1545. position: absolute;
  1546. right: 0;
  1547. bottom: 0;
  1548. background: rgba(0, 0, 0, 0.6);
  1549. color: #fff;
  1550. font-size: 10px;
  1551. padding: 2px 6px;
  1552. border-radius: 4px 0 0 0;
  1553. }
  1554. .tpl-base-info {
  1555. margin-top: 10px;
  1556. }
  1557. .tpl-base-info .name {
  1558. font-weight: bold;
  1559. font-size: 14px;
  1560. margin-bottom: 5px;
  1561. }
  1562. .tpl-base-info .desc {
  1563. font-size: 12px;
  1564. color: #666;
  1565. display: flex;
  1566. align-items: center;
  1567. gap: 5px;
  1568. }
  1569. .color-val-text {
  1570. font-family: monospace;
  1571. color: #409eff;
  1572. font-weight: bold;
  1573. }
  1574. .color-dot {
  1575. width: 12px;
  1576. height: 12px;
  1577. border-radius: 2px;
  1578. }
  1579. .form-group-title {
  1580. font-size: 14px;
  1581. font-weight: bold;
  1582. color: #409eff;
  1583. padding-left: 10px;
  1584. border-left: 4px solid #409eff;
  1585. margin: 15px 0 10px 0;
  1586. background: #f0f7ff;
  1587. padding: 4px 10px;
  1588. border-radius: 2px;
  1589. }
  1590. .mini-upload-box {
  1591. width: 60px;
  1592. height: 36px;
  1593. border: 1px dashed #ddd;
  1594. display: flex;
  1595. align-items: center;
  1596. justify-content: center;
  1597. overflow: hidden;
  1598. border-radius: 4px;
  1599. }
  1600. .mini-upload-box img {
  1601. width: 100%;
  1602. height: 100%;
  1603. object-fit: contain;
  1604. }
  1605. /* Picker Specific Styles */
  1606. .premium-picker-dialog .el-dialog__body {
  1607. padding: 10px 20px 20px;
  1608. }
  1609. .prod-picker-box.fixed-height {
  1610. height: 500px;
  1611. display: flex;
  1612. flex-direction: column;
  1613. }
  1614. .picker-filter-bar {
  1615. display: flex;
  1616. gap: 15px;
  1617. margin-bottom: 20px;
  1618. background: #f9f9f9;
  1619. padding: 15px;
  1620. border-radius: 8px;
  1621. }
  1622. .filter-item.wide-picker {
  1623. width: 400px;
  1624. }
  1625. .search-input {
  1626. width: 400px;
  1627. }
  1628. .picker-prod-thumb {
  1629. width: 50px;
  1630. height: 50px;
  1631. border-radius: 4px;
  1632. border: 1px solid #eee;
  1633. }
  1634. .picker-table {
  1635. border-radius: 8px;
  1636. overflow: hidden;
  1637. flex: 1;
  1638. }
  1639. .price-compare {
  1640. line-height: 1.4;
  1641. }
  1642. .price-compare .m-price {
  1643. font-size: 11px;
  1644. color: #999;
  1645. text-decoration: line-through;
  1646. }
  1647. .price-compare .p-price {
  1648. color: #e3132d;
  1649. font-weight: bold;
  1650. font-size: 14px;
  1651. }
  1652. /* 强制单行不换行 */
  1653. :deep(.el-cascader__tags) {
  1654. flex-wrap: nowrap;
  1655. overflow: hidden;
  1656. }
  1657. :deep(.el-tag) {
  1658. max-width: 180px;
  1659. }
  1660. .picker-pagination {
  1661. margin-top: 20px;
  1662. display: flex;
  1663. justify-content: flex-end;
  1664. }
  1665. .picker-footer-actions {
  1666. margin-top: 20px;
  1667. text-align: center;
  1668. border-top: 1px solid #eee;
  1669. padding-top: 20px;
  1670. }
  1671. .banner-upload-box {
  1672. width: 100%;
  1673. height: 100px;
  1674. border: 1px dashed #ddd;
  1675. border-radius: 8px;
  1676. overflow: hidden;
  1677. position: relative;
  1678. cursor: pointer;
  1679. }
  1680. .banner-upload-box img {
  1681. width: 100%;
  1682. height: 100%;
  1683. object-fit: cover;
  1684. }
  1685. .banner-upload-box .mask {
  1686. position: absolute;
  1687. left: 0;
  1688. top: 0;
  1689. width: 100%;
  1690. height: 100%;
  1691. background: rgba(0, 0, 0, 0.4);
  1692. color: #fff;
  1693. display: flex;
  1694. align-items: center;
  1695. justify-content: center;
  1696. opacity: 0;
  1697. transition: 0.3s;
  1698. }
  1699. .banner-upload-box:hover .mask {
  1700. opacity: 1;
  1701. }
  1702. .color-picker-group {
  1703. display: flex;
  1704. align-items: center;
  1705. gap: 10px;
  1706. }
  1707. .color-text {
  1708. font-family: monospace;
  1709. color: #666;
  1710. font-size: 13px;
  1711. }
  1712. .final-summary {
  1713. text-align: center;
  1714. padding: 40px 0;
  1715. }
  1716. .summary-card {
  1717. display: inline-flex;
  1718. align-items: center;
  1719. gap: 20px;
  1720. background: #f0f9eb;
  1721. border: 1px solid #e1f3d8;
  1722. padding: 30px;
  1723. border-radius: 12px;
  1724. text-align: left;
  1725. margin-bottom: 20px;
  1726. }
  1727. .sum-icon {
  1728. font-size: 40px;
  1729. }
  1730. .sum-text h3 {
  1731. margin: 0 0 10px 0;
  1732. color: #67c23a;
  1733. }
  1734. .sum-text p {
  1735. margin: 5px 0;
  1736. color: #5e6d82;
  1737. }
  1738. .preview-banner-hint {
  1739. color: #909399;
  1740. font-size: 13px;
  1741. margin-bottom: 40px;
  1742. }
  1743. .action-buttons-group {
  1744. display: flex;
  1745. justify-content: center;
  1746. gap: 15px;
  1747. }
  1748. .tpl-tip {
  1749. font-size: 12px;
  1750. color: #ff9900;
  1751. margin-top: 10px;
  1752. font-style: italic;
  1753. }
  1754. .mini-prod-img {
  1755. width: 40px;
  1756. height: 40px;
  1757. border-radius: 4px;
  1758. }
  1759. /* Tpl Picker Grid */
  1760. .tpl-grid {
  1761. display: grid;
  1762. grid-template-columns: repeat(2, 1fr);
  1763. gap: 15px;
  1764. padding: 10px;
  1765. }
  1766. .tpl-grid-item {
  1767. border: 2px solid transparent;
  1768. border-radius: 8px;
  1769. padding: 8px;
  1770. cursor: pointer;
  1771. transition: all 0.2s;
  1772. background: #fdfdfd;
  1773. }
  1774. .tpl-grid-item:hover {
  1775. transform: translateY(-3px);
  1776. box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
  1777. }
  1778. .tpl-grid-item.active {
  1779. border-color: #409eff;
  1780. background: #ecf5ff;
  1781. }
  1782. .grid-img {
  1783. width: 100%;
  1784. height: 120px;
  1785. border-radius: 4px;
  1786. }
  1787. .grid-name {
  1788. font-size: 14px;
  1789. font-weight: 600;
  1790. margin-top: 10px;
  1791. margin-bottom: 4px;
  1792. }
  1793. .grid-layout {
  1794. font-size: 12px;
  1795. color: #999;
  1796. }
  1797. .picker-footer {
  1798. margin-top: 20px;
  1799. text-align: right;
  1800. border-top: 1px solid #eee;
  1801. padding-top: 15px;
  1802. }
  1803. /* Premium Preview Dialog Style (Synced with Product Module) */
  1804. .premium-preview-dialog :deep(.el-overlay-dialog) {
  1805. overflow: hidden;
  1806. display: flex;
  1807. align-items: flex-start;
  1808. justify-content: center;
  1809. }
  1810. .premium-preview-dialog :deep(.el-dialog) {
  1811. margin: 7vh 0 0 0 !important;
  1812. display: flex;
  1813. flex-direction: column;
  1814. max-height: 85vh;
  1815. width: 900px;
  1816. }
  1817. .premium-preview-dialog :deep(.el-dialog__body) {
  1818. padding: 0 !important;
  1819. overflow: hidden;
  1820. background: #fff;
  1821. flex: 1;
  1822. display: flex;
  1823. flex-direction: column;
  1824. }
  1825. .preview-scroll-wrapper {
  1826. display: flex;
  1827. flex-direction: column;
  1828. gap: 35px;
  1829. align-items: center;
  1830. background: #e1e3e6;
  1831. padding: 30px 10px;
  1832. border-radius: 0;
  1833. height: calc(100vh - 320px);
  1834. min-height: 300px;
  1835. overflow-y: auto;
  1836. overflow-x: hidden;
  1837. box-shadow: inset 0 4px 10px rgba(0, 0, 0, 0.05);
  1838. }
  1839. .preview-scroll-wrapper::-webkit-scrollbar {
  1840. width: 8px;
  1841. }
  1842. .preview-scroll-wrapper::-webkit-scrollbar-thumb {
  1843. background: #b0b0b0;
  1844. border-radius: 4px;
  1845. }
  1846. .preview-scroll-wrapper::-webkit-scrollbar-track {
  1847. background: transparent;
  1848. }
  1849. .preview-slide {
  1850. width: 800px;
  1851. height: 450px;
  1852. background: white;
  1853. box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
  1854. position: relative;
  1855. overflow: hidden;
  1856. flex-shrink: 0;
  1857. box-sizing: border-box;
  1858. border: 1px solid #ddd;
  1859. }
  1860. .cover-slide {
  1861. display: flex;
  1862. flex-direction: column;
  1863. background: #fff;
  1864. }
  1865. .cover-top-img-area {
  1866. flex: 0 0 75%;
  1867. position: relative;
  1868. overflow: hidden;
  1869. background: #eee;
  1870. }
  1871. .preview-full-bg {
  1872. width: 100%;
  1873. height: 100%;
  1874. object-fit: cover;
  1875. }
  1876. .preview-bg-mask {
  1877. position: absolute;
  1878. left: 0;
  1879. top: 0;
  1880. width: 100%;
  1881. height: 100%;
  1882. background: rgba(0, 0, 0, 0.5);
  1883. }
  1884. .cover-text-group {
  1885. position: absolute;
  1886. left: 40px;
  1887. top: 55%;
  1888. transform: translateY(-50%);
  1889. z-index: 5;
  1890. color: white;
  1891. text-align: left;
  1892. }
  1893. .c-main-title {
  1894. font-size: 38px;
  1895. font-weight: bold;
  1896. margin: 0 0 10px 0;
  1897. text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
  1898. }
  1899. .c-sub-title {
  1900. font-size: 24px;
  1901. margin: 0 0 15px 0;
  1902. opacity: 0.95;
  1903. }
  1904. .c-validity {
  1905. font-size: 13px;
  1906. color: #eee;
  1907. }
  1908. .cover-bottom-white-bar {
  1909. flex: 1;
  1910. display: flex;
  1911. align-items: center;
  1912. justify-content: space-between;
  1913. padding: 0 35px;
  1914. background: white;
  1915. }
  1916. .bar-left {
  1917. display: flex;
  1918. align-items: center;
  1919. }
  1920. .bar-logo {
  1921. height: 60px;
  1922. max-width: 100px;
  1923. object-fit: contain;
  1924. }
  1925. .bar-v-line {
  1926. width: 1px;
  1927. height: 50px;
  1928. background: #ddd;
  1929. margin: 0 20px;
  1930. }
  1931. .bar-brand-info {
  1932. text-align: left;
  1933. }
  1934. .b-name {
  1935. font-size: 22px;
  1936. font-weight: bold;
  1937. color: #000;
  1938. line-height: 1.2;
  1939. }
  1940. .b-slogan {
  1941. font-size: 11px;
  1942. color: #666;
  1943. margin-top: 4px;
  1944. }
  1945. .bar-right {
  1946. text-align: right;
  1947. }
  1948. .b-contact {
  1949. font-size: 13px;
  1950. font-weight: bold;
  1951. color: #000;
  1952. margin-bottom: 2px;
  1953. }
  1954. .end-slide {
  1955. display: flex;
  1956. align-items: center;
  1957. justify-content: flex-end;
  1958. padding: 0 60px;
  1959. background: #fff;
  1960. position: relative;
  1961. }
  1962. .end-content-right {
  1963. text-align: right;
  1964. z-index: 5;
  1965. }
  1966. .thanks-title {
  1967. font-size: 100px;
  1968. font-weight: 900;
  1969. margin: 0;
  1970. line-height: 1;
  1971. letter-spacing: -2px;
  1972. }
  1973. .end-brand-line {
  1974. margin-top: 20px;
  1975. font-size: 18px;
  1976. font-weight: bold;
  1977. }
  1978. .end-divider {
  1979. margin: 0 5px;
  1980. color: #333;
  1981. }
  1982. .end-b-name {
  1983. color: #333;
  1984. }
  1985. .end-website {
  1986. margin-top: 10px;
  1987. font-size: 16px;
  1988. font-weight: bold;
  1989. }
  1990. .end-deco-cloud {
  1991. position: absolute;
  1992. right: -50px;
  1993. bottom: -80px;
  1994. width: 340px;
  1995. height: 340px;
  1996. border: 50px solid var(--theme-color, #c00000);
  1997. opacity: 0.3;
  1998. border-radius: 50%;
  1999. z-index: 0;
  2000. pointer-events: none;
  2001. }
  2002. .end-deco-cloud::before {
  2003. content: '';
  2004. position: absolute;
  2005. right: 120px;
  2006. bottom: 60px;
  2007. width: 200px;
  2008. height: 200px;
  2009. border: 35px solid var(--theme-color, #c00000);
  2010. opacity: 0.3;
  2011. border-radius: 50%;
  2012. }
  2013. .content-slide {
  2014. background: #fafafa;
  2015. }
  2016. .top-bar {
  2017. position: absolute;
  2018. top: 0;
  2019. left: 0;
  2020. right: 0;
  2021. height: 40px;
  2022. background: #fff;
  2023. border-bottom: 3px solid var(--theme-color);
  2024. display: flex;
  2025. align-items: center;
  2026. padding: 0 20px;
  2027. font-weight: bold;
  2028. color: var(--theme-color);
  2029. font-size: 16px;
  2030. z-index: 10;
  2031. }
  2032. .bottom-deco {
  2033. position: absolute;
  2034. bottom: 0;
  2035. left: 0;
  2036. right: 0;
  2037. height: 8px;
  2038. background: var(--theme-color);
  2039. opacity: 0.15;
  2040. }
  2041. .corner-deco {
  2042. position: absolute;
  2043. bottom: 15px;
  2044. right: -25px;
  2045. width: 100px;
  2046. height: 100px;
  2047. background: var(--theme-color);
  2048. opacity: 0.08;
  2049. transform: rotate(45deg);
  2050. }
  2051. .preview-slide * {
  2052. box-sizing: border-box;
  2053. }
  2054. .layout-1p1 {
  2055. display: flex;
  2056. height: 100%;
  2057. padding: 60px 40px 30px 40px;
  2058. box-sizing: border-box;
  2059. gap: 30px;
  2060. align-items: center;
  2061. justify-content: center;
  2062. }
  2063. .layout-1p1 .img-box {
  2064. flex: 0 0 50%;
  2065. max-width: 50%;
  2066. height: 260px;
  2067. display: flex;
  2068. justify-content: center;
  2069. align-items: center;
  2070. background: #f5f5f5;
  2071. border: 1px solid #eee;
  2072. border-radius: 8px;
  2073. padding: 0;
  2074. overflow: hidden;
  2075. }
  2076. .layout-1p1 .img-box img {
  2077. width: 100%;
  2078. height: 100%;
  2079. object-fit: cover;
  2080. display: block;
  2081. }
  2082. .layout-1p1 .info-box {
  2083. flex: 1;
  2084. display: flex;
  2085. flex-direction: column;
  2086. justify-content: center;
  2087. align-items: flex-start;
  2088. text-align: left;
  2089. }
  2090. .prod-name {
  2091. font-size: 20px;
  2092. font-weight: bold;
  2093. color: #333;
  2094. margin: 0 0 15px 0;
  2095. line-height: 1.4;
  2096. display: -webkit-box;
  2097. -webkit-line-clamp: 3;
  2098. -webkit-box-orient: vertical;
  2099. overflow: hidden;
  2100. }
  2101. .layout-1p1 .info-box p {
  2102. font-size: 13px;
  2103. color: #666;
  2104. margin: 4px 0;
  2105. }
  2106. .layout-1p1 .price-tag {
  2107. background: var(--theme-color);
  2108. color: white;
  2109. padding: 6px 20px;
  2110. border-radius: 6px;
  2111. font-size: 20px;
  2112. font-weight: bold;
  2113. margin-top: 20px;
  2114. }
  2115. .layout-1p2 {
  2116. display: flex;
  2117. flex-direction: column;
  2118. height: 100%;
  2119. padding: 45px 35px 25px 35px;
  2120. box-sizing: border-box;
  2121. }
  2122. .half {
  2123. flex: 1;
  2124. display: flex;
  2125. align-items: center;
  2126. justify-content: space-between;
  2127. gap: 20px;
  2128. }
  2129. .half .img-box {
  2130. flex: 0 0 35%;
  2131. max-width: 35%;
  2132. display: flex;
  2133. justify-content: center;
  2134. align-items: center;
  2135. background: #f5f5f5;
  2136. border: 1px solid #eee;
  2137. border-radius: 6px;
  2138. padding: 0;
  2139. height: 125px;
  2140. overflow: hidden;
  2141. }
  2142. .half .img-box img {
  2143. width: 100%;
  2144. height: 100%;
  2145. object-fit: cover;
  2146. display: block;
  2147. }
  2148. .half .info-box {
  2149. flex: 1;
  2150. display: flex;
  2151. flex-direction: column;
  2152. justify-content: center;
  2153. }
  2154. .upper .info-box {
  2155. align-items: flex-start;
  2156. text-align: left;
  2157. }
  2158. .lower .info-box {
  2159. align-items: flex-end;
  2160. text-align: right;
  2161. }
  2162. .layout-1p2 .prod-name {
  2163. font-size: 16px;
  2164. margin: 0 0 8px 0;
  2165. -webkit-line-clamp: 2;
  2166. }
  2167. .layout-1p2 p {
  2168. margin: 3px 0;
  2169. font-size: 12px;
  2170. color: #888;
  2171. }
  2172. .price-text {
  2173. color: var(--theme-color);
  2174. font-weight: bold;
  2175. font-size: 18px;
  2176. margin-top: 8px;
  2177. }
  2178. .divider {
  2179. border-bottom: 1px dashed #ddd;
  2180. margin: 5px 0;
  2181. }
  2182. .detail-top-bar {
  2183. position: absolute;
  2184. top: 0;
  2185. left: 0;
  2186. right: 0;
  2187. height: 50px;
  2188. background: #fff;
  2189. border-bottom: 1px dotted #ccc;
  2190. display: flex;
  2191. justify-content: space-between;
  2192. align-items: center;
  2193. padding: 0 30px;
  2194. z-index: 10;
  2195. }
  2196. .d-left-title {
  2197. font-size: 18px;
  2198. font-weight: bold;
  2199. color: #111;
  2200. display: flex;
  2201. align-items: center;
  2202. }
  2203. .red-stick {
  2204. width: 4px;
  2205. height: 18px;
  2206. background: #e3132d;
  2207. margin-right: 12px;
  2208. }
  2209. .d-right-logo {
  2210. height: 26px;
  2211. object-fit: contain;
  2212. }
  2213. .layout-1p1-detail {
  2214. display: flex;
  2215. height: 100%;
  2216. padding: 75px 30px 30px 40px;
  2217. box-sizing: border-box;
  2218. gap: 35px;
  2219. }
  2220. .d-img-box {
  2221. flex: 0 0 38%;
  2222. max-width: 38%;
  2223. padding: 5px;
  2224. background: #fff;
  2225. display: flex;
  2226. align-items: center;
  2227. justify-content: center;
  2228. height: 250px;
  2229. overflow: hidden;
  2230. }
  2231. .d-img-box img {
  2232. width: 100%;
  2233. height: 100%;
  2234. object-fit: contain;
  2235. }
  2236. .d-info-box {
  2237. flex: 1;
  2238. display: flex;
  2239. flex-direction: column;
  2240. text-align: left;
  2241. justify-content: flex-start;
  2242. padding-top: 5px;
  2243. }
  2244. .d-prod-name {
  2245. font-size: 15px;
  2246. font-weight: normal;
  2247. color: #000;
  2248. margin: 0 0 6px 0;
  2249. -webkit-line-clamp: 2;
  2250. overflow: hidden;
  2251. display: -webkit-box;
  2252. -webkit-box-orient: vertical;
  2253. }
  2254. .d-red-line {
  2255. width: 100%;
  2256. height: 1px;
  2257. background: #e3132d;
  2258. margin-bottom: 8px;
  2259. }
  2260. .d-base-props p {
  2261. font-size: 12px;
  2262. font-weight: bold;
  2263. color: #111;
  2264. margin: 2px 0;
  2265. line-height: 1.1;
  2266. }
  2267. .strikethrough {
  2268. text-decoration: line-through;
  2269. color: #666;
  2270. }
  2271. .mt-2 {
  2272. margin-top: 5px !important;
  2273. }
  2274. .strong-red {
  2275. color: #e3132d !important;
  2276. font-size: 14px !important;
  2277. margin-top: 1px !important;
  2278. }
  2279. .d-grid-props {
  2280. display: flex;
  2281. margin-top: 6px;
  2282. gap: 10px;
  2283. }
  2284. .d-grid-col {
  2285. flex: 1;
  2286. }
  2287. .d-grid-col h4 {
  2288. font-size: 12px;
  2289. color: #111;
  2290. margin: 0 0 3px 0;
  2291. font-weight: 900;
  2292. }
  2293. .d-list {
  2294. padding: 0;
  2295. margin: 0;
  2296. list-style: none;
  2297. }
  2298. .d-list li {
  2299. font-size: 11px;
  2300. color: #555;
  2301. margin-bottom: 5px;
  2302. line-height: 1.4;
  2303. letter-spacing: -0.3px;
  2304. }
  2305. .s-title-row {
  2306. display: flex;
  2307. align-items: center;
  2308. gap: 10px;
  2309. margin-bottom: 4px;
  2310. }
  2311. .scheme-name-box .s-name {
  2312. font-weight: 600;
  2313. color: #409eff;
  2314. font-size: 15px;
  2315. cursor: pointer;
  2316. transition: 0.2s;
  2317. white-space: nowrap;
  2318. }
  2319. .scheme-name-box .s-name:hover {
  2320. color: #66b1ff;
  2321. text-decoration: underline;
  2322. }
  2323. .s-meta-line {
  2324. font-size: 12px;
  2325. color: #999;
  2326. display: flex;
  2327. align-items: center;
  2328. gap: 5px;
  2329. }
  2330. .s-meta-line .label {
  2331. color: #666;
  2332. font-weight: 500;
  2333. }
  2334. .s-meta-line .divider {
  2335. color: #eee;
  2336. margin: 0 2px;
  2337. }
  2338. .s-share-line {
  2339. font-size: 11px;
  2340. color: #67c23a;
  2341. display: flex;
  2342. align-items: center;
  2343. gap: 4px;
  2344. background: #f0f9eb;
  2345. padding: 2px 8px;
  2346. border-radius: 4px;
  2347. width: fit-content;
  2348. flex-shrink: 0;
  2349. }
  2350. /* Share Drawer Styles */
  2351. .share-panel-drawer :deep(.el-drawer__body) {
  2352. padding: 0;
  2353. }
  2354. .share-drawer-body {
  2355. height: 100%;
  2356. display: flex;
  2357. flex-direction: column;
  2358. background: #f8f9fa;
  2359. }
  2360. .share-search-section {
  2361. padding: 20px;
  2362. background: #fff;
  2363. border-bottom: 1px solid #eee;
  2364. }
  2365. .premium-search-input :deep(.el-input__wrapper) {
  2366. border-radius: 8px;
  2367. background: #f5f7fa;
  2368. box-shadow: none !important;
  2369. border: 1px solid #e4e7ed;
  2370. }
  2371. .share-main-content {
  2372. flex: 1;
  2373. display: flex;
  2374. flex-direction: column;
  2375. overflow: hidden;
  2376. }
  2377. .selection-container {
  2378. flex: 1;
  2379. display: flex;
  2380. flex-direction: column;
  2381. overflow: hidden;
  2382. padding: 20px;
  2383. }
  2384. .section-title {
  2385. font-size: 14px;
  2386. font-weight: bold;
  2387. color: #303133;
  2388. margin: 0 0 15px 0;
  2389. }
  2390. .employee-list-scroll {
  2391. flex: 1;
  2392. overflow-y: auto;
  2393. padding-right: 5px;
  2394. }
  2395. .emp-item {
  2396. display: flex;
  2397. align-items: center;
  2398. padding: 12px;
  2399. background: #fff;
  2400. border-radius: 8px;
  2401. margin-bottom: 8px;
  2402. cursor: pointer;
  2403. transition: all 0.2s;
  2404. border: 1px solid transparent;
  2405. }
  2406. .emp-item:hover {
  2407. background: #f0f7ff;
  2408. transform: translateX(5px);
  2409. }
  2410. .emp-item.is-selected {
  2411. border-color: #409eff;
  2412. background: #f0f7ff;
  2413. }
  2414. .emp-avatar {
  2415. background: #409eff;
  2416. color: #fff;
  2417. margin-right: 12px;
  2418. }
  2419. .emp-info {
  2420. flex: 1;
  2421. }
  2422. .e-name {
  2423. font-size: 14px;
  2424. font-weight: bold;
  2425. color: #303133;
  2426. }
  2427. .e-dept {
  2428. font-size: 12px;
  2429. color: #909399;
  2430. margin-top: 2px;
  2431. }
  2432. .check-icon {
  2433. color: #409eff;
  2434. font-size: 18px;
  2435. }
  2436. .selected-summary {
  2437. padding: 15px 20px;
  2438. background: #fff;
  2439. border-top: 1px solid #eee;
  2440. border-bottom: 1px solid #eee;
  2441. }
  2442. .summary-header {
  2443. font-size: 13px;
  2444. font-weight: bold;
  2445. color: #606266;
  2446. margin-bottom: 10px;
  2447. }
  2448. .summary-tags {
  2449. display: flex;
  2450. flex-wrap: wrap;
  2451. gap: 8px;
  2452. }
  2453. .m-2 {
  2454. margin: 0 !important;
  2455. }
  2456. .share-footer {
  2457. display: flex;
  2458. justify-content: flex-end;
  2459. gap: 12px;
  2460. width: 100%;
  2461. }
  2462. .fade-in {
  2463. animation: fadeIn 0.3s ease-in;
  2464. }
  2465. @keyframes fadeIn {
  2466. from {
  2467. opacity: 0;
  2468. transform: translateY(10px);
  2469. }
  2470. to {
  2471. opacity: 1;
  2472. transform: translateY(0);
  2473. }
  2474. }
  2475. </style>