config-game.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  1. <template>
  2. <modal-pro
  3. :label="getModalTitle()"
  4. :open="open"
  5. :mask="false"
  6. width="540px"
  7. @close="emits('update:open', false)"
  8. @ok="saveGameConfig"
  9. styles="position: fixed;top: 60px;right: 90px; pointer-events: none;"
  10. >
  11. <template #title>
  12. <div style="display: flex;justify-content: space-between;" >
  13. <div>游戏配置</div>
  14. <a-button v-if="currentLevel === 0" type="primary" size="small" @click="createGameOne">
  15. <template #icon><plus-outlined /></template>
  16. 新增
  17. </a-button>
  18. </div>
  19. </template>
  20. <div class="config-game-page">
  21. <div class="game-modal-content">
  22. <!-- 显示返回按钮和新增按钮,仅在非顶级目录显示 -->
  23. <div v-if="currentLevel > 0" class="back-button-row">
  24. <a-button type="link" @click="goBack">
  25. <template #icon><left-outlined /></template>
  26. 返回上级
  27. </a-button>
  28. <a-button v-if="currentLevel === 1" type="primary" size="small" @click="createSubItem">
  29. <template #icon><plus-outlined /></template>
  30. 新增
  31. </a-button>
  32. </div>
  33. <!-- 顶级文件夹内容 -->
  34. <a-row v-if="currentLevel === 0" justify="flex-start" :gutter="[8, 8]" class="config-game-list">
  35. <a-col
  36. :span="7"
  37. style="height: 120px;"
  38. class="config-game-item"
  39. v-for="(folder, index) in cardJson.game_list"
  40. :key="index"
  41. @dblclick="enterFolder(index)"
  42. >
  43. <!-- 状态图标 -->
  44. <div class="status-icon">
  45. <CheckCircleFilled v-if="validateGameConfig(folder).isValid" class="status-complete" />
  46. <ExclamationCircleFilled v-else class="status-incomplete" />
  47. </div>
  48. <img class="folder-icon" :src="require('@/assets/common/folder.svg')" alt="">
  49. <div class="folder-footer">
  50. <div>游戏 {{ index + 1 }}</div>
  51. <div>
  52. <a-dropdown :trigger="['hover']">
  53. <SmallDashOutlined class="action-icon" />
  54. <template #overlay>
  55. <a-menu>
  56. <a-menu-item key="edit" @click="handleEdit(index)">
  57. <EditOutlined />
  58. <span style="margin-left: 8px;">编辑</span>
  59. </a-menu-item>
  60. <a-menu-item key="delete" @click="handleDelete(index)">
  61. <DeleteOutlined style="color: #ff4d4f;" />
  62. <span style="margin-left: 8px;">删除</span>
  63. </a-menu-item>
  64. </a-menu>
  65. </template>
  66. </a-dropdown>
  67. </div>
  68. </div>
  69. </a-col>
  70. <a-empty style="margin: 0 auto;" v-if="cardJson.game_list.length === 0" description="暂无游戏"> </a-empty>
  71. </a-row>
  72. <div v-else-if="currentLevel === 1">
  73. <a-row class="config-game-list" :gutter="[8, 8]">
  74. <VueDraggableNext
  75. :list="cardJson.game_list[currentGameIndex].items"
  76. group="people"
  77. item-key="id"
  78. :animation="300"
  79. class="config-game-list"
  80. >
  81. <a-col
  82. :span="10"
  83. class="config-game-item"
  84. v-for="(item, index) in cardJson.game_list[currentGameIndex].items"
  85. :key="index"
  86. @dblclick="enterCardConfig(index)"
  87. >
  88. <img class="folder-icon" :src="require('@/assets/common/folder.svg')" alt="">
  89. <div class="folder-info">
  90. <div v-if="item.sub_subject?.music_name" class="music-badge">
  91. <sound-outlined />
  92. <span class="music-name">{{ item.sub_subject.music_name }}</span>
  93. </div>
  94. </div>
  95. <div class="folder-footer">
  96. <div>游戏 {{ index + 1 }}</div>
  97. <div>
  98. <a-dropdown :trigger="['hover']">
  99. <SmallDashOutlined class="action-icon" />
  100. <template #overlay>
  101. <a-menu>
  102. <a-menu-item key="edit" @click="handleEditSubItem(index)">
  103. <EditOutlined />
  104. <span style="margin-left: 8px;">编辑</span>
  105. </a-menu-item>
  106. <a-menu-item key="delete" @click="handleDeleteSubItem(index)">
  107. <DeleteOutlined style="color: #ff4d4f;" />
  108. <span style="margin-left: 8px;">删除</span>
  109. </a-menu-item>
  110. </a-menu>
  111. </template>
  112. </a-dropdown>
  113. </div>
  114. </div>
  115. </a-col>
  116. </VueDraggableNext>
  117. </a-row>
  118. <a-empty style="margin: 0 auto;" v-if="cardJson.game_list[currentGameIndex].items.length === 0" description="暂无游戏"> </a-empty>
  119. </div>
  120. <!-- 三级内容:游戏步骤配置 -->
  121. <div v-else-if="currentLevel === 2">
  122. <div class="card-config-content">
  123. <gameStage3
  124. v-if="cardJson.game_list[currentGameIndex]?.items?.[currentSubFolderIndex]"
  125. v-model="cardJson.game_list[currentGameIndex].items[currentSubFolderIndex]"
  126. :rule="cardJson.game_list[currentGameIndex].rule"
  127. @update:modelValue="changedSteps"
  128. @step-selected="emits('step-selected', $event)"
  129. />
  130. </div>
  131. </div>
  132. </div>
  133. </div>
  134. </modal-pro>
  135. <!-- 新增子项目弹窗 -->
  136. <a-modal
  137. v-model:open="subItemModalVisible"
  138. title="新增子项目"
  139. width="400px"
  140. @ok="handleSaveSubItem"
  141. >
  142. <a-form :model="subItemForm" layout="vertical">
  143. <a-form-item label="子主题音乐">
  144. <SelectAudioNew
  145. v-model="subItemForm.sub_subject.music_name"
  146. placeholder="请选择子主题音乐"
  147. />
  148. <!-- 已选择的音乐名称展示区域 -->
  149. <div v-if="subItemForm.sub_subject.music_name" class="selected-music-display">
  150. <div class="music-info">
  151. <sound-outlined />
  152. <span class="music-name">{{ subItemForm.sub_subject.music_name }}</span>
  153. </div>
  154. </div>
  155. </a-form-item>
  156. </a-form>
  157. </a-modal>
  158. <!-- 编辑子项目弹窗 -->
  159. <a-modal
  160. v-model:open="editSubItemModalVisible"
  161. title="编辑子项目"
  162. width="400px"
  163. @ok="handleSaveSubItemEdit"
  164. @cancel="handleCancelSubItemEdit"
  165. >
  166. <a-form :model="editSubItemForm" layout="vertical">
  167. <a-form-item label="子主题音乐">
  168. <SelectAudioNew
  169. v-model="editSubItemForm.sub_subject.music_name"
  170. placeholder="请选择子主题音乐"
  171. />
  172. <!-- 已选择的音乐名称展示区域 -->
  173. <div v-if="editSubItemForm.sub_subject.music_name" class="selected-music-display">
  174. <div class="music-info">
  175. <sound-outlined />
  176. <span class="music-name">{{ editSubItemForm.sub_subject.music_name }}</span>
  177. </div>
  178. </div>
  179. </a-form-item>
  180. </a-form>
  181. </a-modal>
  182. <!-- 编辑游戏弹窗 -->
  183. <a-modal
  184. v-model:open="editModalVisible"
  185. title="编辑游戏配置"
  186. width="600px"
  187. @ok="handleSaveEdit"
  188. @cancel="handleCancelEdit"
  189. >
  190. <a-form :model="editForm" layout="vertical">
  191. <!-- 规则单独一行 -->
  192. <a-form-item label="规则">
  193. <a-select v-model:value="editForm.rule" placeholder="请选择规则">
  194. <a-select-option v-for="rule in ruleOptions" :key="rule.value" :value="rule.value">
  195. {{ rule.title }}
  196. </a-select-option>
  197. </a-select>
  198. </a-form-item>
  199. <!-- 音频选择器两列布局 -->
  200. <a-row :gutter="16">
  201. <a-col :span="12">
  202. <a-form-item label="主题音乐:">
  203. <SelectAudioNew
  204. v-model="editForm.mainSubject.music_name"
  205. placeholder="请选择主题音乐"
  206. />
  207. </a-form-item>
  208. </a-col>
  209. <a-col :span="12">
  210. <a-form-item label="顺序多选错误音乐:">
  211. <SelectAudioNew
  212. v-model="editForm.ordered_multiple_err.music_name"
  213. placeholder="请选择顺序多选错误音乐"
  214. />
  215. </a-form-item>
  216. </a-col>
  217. </a-row>
  218. <a-row :gutter="16">
  219. <a-col :span="12">
  220. <a-form-item label="重复单击音乐:">
  221. <SelectAudioNew
  222. v-model="editForm.has_click_single.music_name"
  223. placeholder="请选择重复点击音乐"
  224. />
  225. </a-form-item>
  226. </a-col>
  227. <a-col :span="12">
  228. <a-form-item label="再次点击音乐:">
  229. <SelectAudioNew
  230. v-model="editForm.still_have.music_name"
  231. placeholder="请选择再次点击音乐"
  232. />
  233. </a-form-item>
  234. </a-col>
  235. </a-row>
  236. <a-row :gutter="16">
  237. <a-col :span="12">
  238. <a-form-item label="重复组点击音乐:">
  239. <SelectAudioNew
  240. v-model="editForm.has_click_group.music_name"
  241. placeholder="请选择重复组点击音乐乐"
  242. />
  243. </a-form-item>
  244. </a-col>
  245. <a-col :span="12">
  246. <a-form-item label="等待30秒音乐:">
  247. <SelectAudioNew
  248. v-model="editForm.wait_30s.music_name"
  249. placeholder="请选择等待30秒音乐"
  250. />
  251. </a-form-item>
  252. </a-col>
  253. </a-row>
  254. <a-row :gutter="16">
  255. <a-col :span="12">
  256. <a-form-item label="等待90秒音乐:">
  257. <SelectAudioNew
  258. v-model="editForm.wait_90s.music_name"
  259. placeholder="请选择等待90秒音乐"
  260. />
  261. </a-form-item>
  262. </a-col>
  263. <a-col :span="12">
  264. <!-- 空列,保持对称 -->
  265. </a-col>
  266. </a-row>
  267. </a-form>
  268. </a-modal>
  269. </template>
  270. <script lang='ts' setup>
  271. import { CardController } from '@/controller'
  272. import { LeftOutlined, PlusOutlined, SmallDashOutlined, EditOutlined, DeleteOutlined, CheckCircleFilled, ExclamationCircleFilled, SoundOutlined } from '@ant-design/icons-vue'
  273. import { reactive, ref, PropType, computed, watch } from 'vue'
  274. import { message } from 'ant-design-vue'
  275. import SelectAudioNew from './select-audio-new.vue'
  276. import gameStage3 from './game-stage-3.vue'
  277. import { VueDraggableNext } from 'vue-draggable-next'
  278. const props = defineProps({
  279. open: {
  280. type: Boolean,
  281. default: false
  282. },
  283. config: {
  284. type: Object as PropType<API.CardJson21>,
  285. default: () => ({})
  286. }
  287. })
  288. const emits = defineEmits(['update:open', 'step-selected', 'update:config'])
  289. // 卡片category为21的json
  290. const cardJson = computed({
  291. get: () => props.config,
  292. set: (value) => emits('update:config', value)
  293. })
  294. // 一级folders的form
  295. const game1FormJson = {
  296. rule: '',
  297. main_subject: { music_name: '', is_break: 1 },
  298. ordered_multiple_err: { music_name: '', is_break: 1 },
  299. has_click_single: { music_name: '', is_break: 1 },
  300. still_have: { music_name: '', is_break: 1 },
  301. has_click_group: { music_name: '', is_break: 1 },
  302. wait_30s: { music_name: '', is_break: 1 },
  303. wait_90s: { music_name: '', is_break: 1 }
  304. }
  305. // 当前层级:0表示顶级文件夹,1表示sub文件夹,2表示游戏配置
  306. const currentLevel = ref(0)
  307. // 规则下拉选项
  308. const ruleOptions = CardController.ruleList
  309. // 编辑弹窗相关
  310. const editModalVisible = ref(false)
  311. const editingIndex = ref(-1)
  312. const editForm = reactive({
  313. rule: '',
  314. mainSubject: { music_name: '', is_break: 1 },
  315. ordered_multiple_err: { music_name: '', is_break: 1 },
  316. has_click_single: { music_name: '', is_break: 1 },
  317. still_have: { music_name: '', is_break: 1 },
  318. has_click_group: { music_name: '', is_break: 1 },
  319. wait_30s: { music_name: '', is_break: 1 },
  320. wait_90s: { music_name: '', is_break: 1 }
  321. })
  322. // 编辑子项目弹窗相关
  323. const editSubItemModalVisible = ref(false)
  324. const editingSubItemIndex = ref(-1)
  325. const editSubItemForm = reactive({
  326. sub_subject: {
  327. music_name: '',
  328. mb: '',
  329. ok: '',
  330. ob: '',
  331. err: '',
  332. eb: ''
  333. },
  334. ok_key: [],
  335. err_key: [],
  336. ok_key_voice: {},
  337. err_key_voice: {}
  338. })
  339. // 子项目弹窗相关
  340. const subItemModalVisible = ref(false)
  341. const subItemForm = reactive({
  342. sub_subject: {
  343. music_name: '',
  344. mb: '',
  345. ok: '',
  346. ob: '',
  347. err: '',
  348. eb: ''
  349. },
  350. ok_key: [],
  351. err_key: [],
  352. ok_key_voice: {},
  353. err_key_voice: {}
  354. })
  355. // 当前选中的游戏索引
  356. const currentGameIndex = ref(0)
  357. // 当前选中的子文件夹索引
  358. const currentSubFolderIndex = ref(0)
  359. // 获取弹窗标题
  360. const getModalTitle = () => {
  361. switch (currentLevel.value) {
  362. case 0:
  363. return '游戏配置'
  364. case 1:
  365. return '游戏配置'
  366. case 2:
  367. return '游戏配置'
  368. default:
  369. return '游戏配置'
  370. }
  371. }
  372. const createGameOne = () => {
  373. cardJson.value.game_list.push(JSON.parse(JSON.stringify(game1FormJson)))
  374. }
  375. // 校验游戏配置是否完整
  376. const validateGameConfig = (gameConfig: any) => {
  377. const requiredFields = [
  378. 'rule',
  379. 'main_subject.music_name',
  380. 'ordered_multiple_err.music_name',
  381. 'has_click_single.music_name',
  382. 'still_have.music_name',
  383. 'has_click_group.music_name',
  384. 'wait_30s.music_name',
  385. 'wait_90s.music_name'
  386. ]
  387. const missingFields: string[] = []
  388. requiredFields.forEach(field => {
  389. const fieldParts = field.split('.')
  390. let value = gameConfig
  391. for (const part of fieldParts) {
  392. value = value?.[part]
  393. }
  394. if (!value || value === '') {
  395. missingFields.push(field)
  396. }
  397. })
  398. return {
  399. isValid: missingFields.length === 0,
  400. missingFields
  401. }
  402. }
  403. // 进入二级文件夹
  404. const enterFolder = (index: number) => {
  405. const gameConfig = cardJson.value.game_list[index]
  406. const validation = validateGameConfig(gameConfig)
  407. if (!validation.isValid) {
  408. const fieldNames = {
  409. rule: '规则',
  410. 'mainSubject.music_name': '主题音乐',
  411. 'ordered_multiple_err.music_name': '顺序多选错误音乐',
  412. 'has_click_single.music_name': '重复单击音乐',
  413. 'still_have.music_name': '再次点击音乐',
  414. 'has_click_group.music_name': '重复组点击音乐',
  415. 'wait_30s.music_name': '等待30秒音乐',
  416. 'wait_90s.music_name': '等待90秒音乐'
  417. }
  418. const missingFieldNames = validation.missingFields.map(field => fieldNames[field] || field)
  419. // 使用 Ant Design 的 message 组件显示错误信息
  420. message.error(`游戏 ${index + 1} 配置不完整,缺少以下字段:${missingFieldNames.join('、').replaceAll('、', '、\n')}`)
  421. return
  422. }
  423. // 验证通过,创建 items 数组(如果不存在)
  424. if (!cardJson.value.game_list[index].items) {
  425. cardJson.value.game_list[index].items = []
  426. }
  427. currentGameIndex.value = index
  428. currentLevel.value = 1
  429. }
  430. const changedSteps = (steps: any) => {
  431. console.log('changedSteps:', cardJson.value.game_list[currentGameIndex.value].items)
  432. }
  433. const createSubItem = () => {
  434. // 重置表单并打开弹窗
  435. subItemForm.sub_subject.music_name = ''
  436. subItemForm.sub_subject.mb = ''
  437. subItemForm.sub_subject.ok = ''
  438. subItemForm.sub_subject.ob = ''
  439. subItemForm.sub_subject.err = ''
  440. subItemForm.sub_subject.eb = ''
  441. subItemForm.ok_key = []
  442. subItemForm.err_key = []
  443. subItemForm.ok_key_voice = {}
  444. subItemForm.err_key_voice = {}
  445. subItemModalVisible.value = true
  446. }
  447. // 新建二级文件夹
  448. const handleSaveSubItem = () => {
  449. // 基础校验:必须选择子主题音乐
  450. if (!subItemForm.sub_subject.music_name) {
  451. message.warning('请选择子主题音乐')
  452. return
  453. }
  454. const index = currentGameIndex.value
  455. const currentGame = cardJson.value.game_list[index]
  456. if (!currentGame) {
  457. message.error('未选择有效的游戏')
  458. return
  459. }
  460. // 确保 items 数组存在
  461. if (!currentGame.items) {
  462. currentGame.items = []
  463. }
  464. // 构造 items 下的新对象(遵循 API.CardJson21[game_list].items 的结构)
  465. const newItem = {
  466. sub_subject: {
  467. music_name: subItemForm.sub_subject.music_name,
  468. mb: 1,
  469. ok: '',
  470. ob: 1,
  471. err: '',
  472. eb: 1
  473. },
  474. ok_key: [],
  475. err_key: [],
  476. ok_key_voice: {},
  477. err_key_voice: {}
  478. }
  479. // 追加到 items
  480. currentGame.items.push(newItem)
  481. // 关闭弹窗并重置关键字段,便于下次新增
  482. subItemModalVisible.value = false
  483. subItemForm.sub_subject.music_name = ''
  484. }
  485. // 进入卡片配置
  486. const enterCardConfig = (index: number) => {
  487. currentSubFolderIndex.value = index
  488. const currentItem = cardJson.value.game_list[currentGameIndex.value]?.items?.[index]
  489. if (currentItem) {
  490. if (!currentItem.steps) {
  491. currentItem.steps = []
  492. }
  493. }
  494. currentLevel.value = 2
  495. }
  496. // 返回上级文件夹
  497. const goBack = () => {
  498. if (currentLevel.value > 0) {
  499. currentLevel.value--
  500. }
  501. }
  502. // 处理编辑游戏
  503. const handleEdit = (index: number) => {
  504. editingIndex.value = index
  505. // 将当前游戏数据复制到编辑表单
  506. Object.assign(editForm, JSON.parse(JSON.stringify(cardJson.value.game_list[index])))
  507. editModalVisible.value = true
  508. }
  509. // 处理删除游戏
  510. const handleDelete = (index: number) => {
  511. // 从游戏列表中删除指定索引的游戏
  512. cardJson.value.game_list.splice(index, 1)
  513. }
  514. // 处理编辑子项目
  515. const handleEditSubItem = (index: number) => {
  516. editingSubItemIndex.value = index
  517. // 将当前子项目数据复制到编辑表单
  518. const currentItem = cardJson.value.game_list[currentGameIndex.value].items[index]
  519. editSubItemForm.sub_subject.music_name = currentItem.sub_subject.music_name
  520. editSubItemForm.sub_subject.mb = currentItem.sub_subject.mb
  521. editSubItemForm.sub_subject.ok = currentItem.sub_subject.ok
  522. editSubItemForm.sub_subject.ob = currentItem.sub_subject.ob
  523. editSubItemForm.sub_subject.err = currentItem.sub_subject.err
  524. editSubItemForm.sub_subject.eb = currentItem.sub_subject.eb
  525. editSubItemModalVisible.value = true
  526. }
  527. // 处理删除子项目
  528. const handleDeleteSubItem = (index: number) => {
  529. // 从子项目列表中删除指定索引的项目
  530. cardJson.value.game_list[currentGameIndex.value].items.splice(index, 1)
  531. }
  532. // 保存子项目编辑
  533. const handleSaveSubItemEdit = () => {
  534. if (editingSubItemIndex.value >= 0) {
  535. // 基础校验:必须选择子主题音乐
  536. if (!editSubItemForm.sub_subject.music_name) {
  537. message.warning('请选择子主题音乐')
  538. return
  539. }
  540. // 将编辑表单的数据保存到子项目列表
  541. const currentItem = cardJson.value.game_list[currentGameIndex.value].items[editingSubItemIndex.value]
  542. currentItem.sub_subject.music_name = editSubItemForm.sub_subject.music_name
  543. currentItem.sub_subject.mb = editSubItemForm.sub_subject.mb || 1
  544. currentItem.sub_subject.ok = editSubItemForm.sub_subject.ok || ''
  545. currentItem.sub_subject.ob = editSubItemForm.sub_subject.ob || 1
  546. currentItem.sub_subject.err = editSubItemForm.sub_subject.err || ''
  547. currentItem.sub_subject.eb = editSubItemForm.sub_subject.eb || 1
  548. }
  549. editSubItemModalVisible.value = false
  550. editingSubItemIndex.value = -1
  551. }
  552. // 取消子项目编辑
  553. const handleCancelSubItemEdit = () => {
  554. editSubItemModalVisible.value = false
  555. editingSubItemIndex.value = -1
  556. }
  557. // 保存编辑
  558. const handleSaveEdit = () => {
  559. if (editingIndex.value >= 0) {
  560. // 将编辑表单的数据保存到游戏列表
  561. Object.assign(cardJson.value.game_list[editingIndex.value], JSON.parse(JSON.stringify(editForm)))
  562. }
  563. editModalVisible.value = false
  564. editingIndex.value = -1
  565. }
  566. // 取消编辑
  567. const handleCancelEdit = () => {
  568. editModalVisible.value = false
  569. editingIndex.value = -1
  570. }
  571. // 保存游戏配置
  572. const saveGameConfig = async () => {
  573. console.log('保存游戏配置:', CardController.stepsToItems(cardJson.value))
  574. const defaultJson = await CardController.getDefaultJson()
  575. CardController.add({
  576. header: {
  577. card_type: 21,
  578. ...cardJson.value.header
  579. },
  580. game_list: CardController.stepsToItems(cardJson.value).game_list
  581. })
  582. // 在这里可以添加保存逻辑,比如调用API保存cardJson
  583. // emit('update:open', false)
  584. }
  585. </script>
  586. <style lang='less' scoped>
  587. .create-folder-form {
  588. padding: 16px;
  589. background-color: #fff;
  590. border: 1px solid #d9d9d9;
  591. border-radius: 4px;
  592. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  593. }
  594. .config-game-page {
  595. width: 100%;
  596. height: 100%;
  597. }
  598. .back-button-row {
  599. display: flex;
  600. justify-content: space-between;
  601. align-items: center;
  602. margin-bottom: 16px;
  603. }
  604. .game-modal-content {
  605. padding: 16px;
  606. .config-game-list {
  607. width: 100%;
  608. display: flex;
  609. flex-wrap: wrap;
  610. justify-content: flex-start;
  611. gap: 16px;
  612. height: 100%;
  613. }
  614. .config-game-item {
  615. display: flex;
  616. flex-direction: column;
  617. justify-content: space-between;
  618. align-items: center;
  619. cursor: pointer;
  620. border-radius: 8px;
  621. // transition: all 0.3s;
  622. background-color: #f5f5f5;
  623. padding: 0px !important;
  624. border: 1px solid #e9ecf1;
  625. box-sizing: border-box !important;
  626. margin-right: 16px;
  627. margin-bottom: 16px;
  628. position: relative;
  629. overflow: hidden;
  630. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  631. .status-icon {
  632. position: absolute;
  633. top: 8px;
  634. left: 8px;
  635. z-index: 10;
  636. font-size: 16px;
  637. .status-complete {
  638. color: #52c41a;
  639. }
  640. .status-incomplete {
  641. color: #ff4d4f;
  642. }
  643. }
  644. .folder-icon {
  645. width: 86px;
  646. height: 86px;
  647. }
  648. .folder-footer {
  649. width: 100%;
  650. height: 32px;
  651. display: flex;
  652. align-items: center;
  653. justify-content: space-between;
  654. border-top: 1px solid #e9ecf1;
  655. background-color: #fff;
  656. box-sizing: border-box;
  657. padding: 0 12px;
  658. position: absolute;
  659. bottom: 0;
  660. left: 0;
  661. right: 0;
  662. .action-icon {
  663. cursor: pointer;
  664. opacity: 0;
  665. transition: opacity 0.3s;
  666. font-size: 16px;
  667. color: #8c8c8c;
  668. &:hover {
  669. color: #1890ff;
  670. }
  671. }
  672. }
  673. .folder-info {
  674. width: 100%;
  675. display: flex;
  676. flex-direction: column;
  677. align-items: center;
  678. padding: 12px 0;
  679. margin-bottom: 32px;
  680. .folder-title {
  681. font-weight: 500;
  682. margin-bottom: 8px;
  683. font-size: 14px;
  684. color: #333;
  685. }
  686. .music-badge {
  687. display: flex;
  688. align-items: center;
  689. gap: 4px;
  690. background-color: #e6f7ff;
  691. border-radius: 12px;
  692. padding: 3px 10px;
  693. font-size: 12px;
  694. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  695. .anticon {
  696. color: #1890ff;
  697. font-size: 12px;
  698. }
  699. .music-name {
  700. color: #1890ff;
  701. white-space: nowrap;
  702. overflow: hidden;
  703. text-overflow: ellipsis;
  704. max-width: 80px;
  705. }
  706. }
  707. }
  708. &:hover {
  709. scale: 1.02;
  710. .folder-footer .action-icon {
  711. opacity: 1;
  712. }
  713. }
  714. &:hover {
  715. scale: 1.02;
  716. }
  717. }
  718. }
  719. .card-config-content {
  720. padding: 16px;
  721. h3 {
  722. margin-bottom: 24px;
  723. text-align: center;
  724. }
  725. }
  726. .step-list {
  727. display: flex;
  728. flex-direction: column;
  729. gap: 12px;
  730. }
  731. .step-item {
  732. display: flex;
  733. align-items: center;
  734. padding: 12px;
  735. border: 1px solid #d9d9d9;
  736. border-radius: 4px;
  737. cursor: grab;
  738. background-color: #fff;
  739. transition: all 0.3s;
  740. &:hover {
  741. border-color: #40a9ff;
  742. }
  743. }
  744. .step-item-active {
  745. border-color: #1890ff;
  746. box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
  747. }
  748. .step-number {
  749. font-weight: 500;
  750. margin-right: 16px;
  751. }
  752. .step-circles {
  753. display: flex;
  754. gap: 8px;
  755. flex-grow: 1;
  756. }
  757. .circle {
  758. width: 20px;
  759. height: 20px;
  760. border-radius: 50%;
  761. background-color: #f0f0f0;
  762. border: 1px solid #ccc;
  763. }
  764. .step-actions {
  765. cursor: pointer;
  766. }
  767. .selected-music-display {
  768. margin-top: 12px;
  769. padding: 10px 12px;
  770. background-color: #f6f8fa;
  771. border-radius: 6px;
  772. border: 1px solid #e1e4e8;
  773. transition: all 0.3s;
  774. }
  775. .music-info {
  776. display: flex;
  777. align-items: center;
  778. gap: 8px;
  779. .anticon {
  780. color: #1890ff;
  781. font-size: 16px;
  782. }
  783. .music-name {
  784. color: #333;
  785. font-size: 14px;
  786. white-space: nowrap;
  787. overflow: hidden;
  788. text-overflow: ellipsis;
  789. max-width: 300px;
  790. }
  791. }
  792. </style>