DetailPropsEditor.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. <template>
  2. <div class="detail-props-editor">
  3. <a-form :labelCol="{ span: 6 }" size="small">
  4. <a-form-item label="测试内容ID">
  5. <a-input-number
  6. :value="props.testDetailId"
  7. @update:value="emit('update:testDetailId', $event)"
  8. style="width: 100%"
  9. />
  10. </a-form-item>
  11. <a-form-item label="显示头部">
  12. <a-checkbox v-model:checked="props.props.showHead" :indeterminate="props.props.showHead === undefined">
  13. 默认:显示
  14. </a-checkbox>
  15. </a-form-item>
  16. <a-form-item label="显示级别标签">
  17. <a-checkbox v-model:checked="props.props.showTag" />
  18. </a-form-item>
  19. <a-form-item label="显示死亡框">
  20. <a-checkbox v-model:checked="props.props.showDeadBox" />
  21. </a-form-item>
  22. </a-form>
  23. <a-collapse v-model:activeKey="activeKeys" class="props-collapse">
  24. <a-collapse-panel key="introBlockDescs" header="简介块描述项 (introBlockDescs)">
  25. <div v-for="(item, i) in introBlockDescsList" :key="`desc-${i}`" class="nested-item">
  26. <div class="item-actions">
  27. <ArrowUpOutlined title="上移" @click.stop="moveIntroBlockDescUp(i)" />
  28. <ArrowDownOutlined title="下移" @click.stop="moveIntroBlockDescDown(i)" />
  29. <CopyOutlined title="复制" @click.stop="copyIntroBlockDesc(i)" />
  30. </div>
  31. <a-form :labelCol="{ span: 6 }" size="small">
  32. <a-form-item label="label">
  33. <a-input v-model:value="item.label" placeholder="显示标签" />
  34. </a-form-item>
  35. <a-form-item label="key">
  36. <a-input v-model:value="item.key" placeholder="数据键名" />
  37. </a-form-item>
  38. <a-form-item label="映射关系">
  39. <KeyValueEditor v-model:value="item.map" />
  40. </a-form-item>
  41. <a-popconfirm title="确定删除?" @confirm="removeIntroBlockDesc(i)">
  42. <a-button type="link" danger size="small">删除</a-button>
  43. </a-popconfirm>
  44. </a-form>
  45. </div>
  46. <div class="section-footer">
  47. <a-button style="flex: 4;" type="dashed" block size="small" @click="addIntroBlockDesc">+ 添加描述项</a-button>
  48. <a-button style="flex: 2;" type="dashed" block size="small" @click="pasteIntroBlockDesc">粘贴</a-button>
  49. </div>
  50. </a-collapse-panel>
  51. <a-collapse-panel key="introBlocks" header="简介下方块 (introBlocks)">
  52. <div v-for="(item, i) in introBlocksList" :key="`block-${i}`" class="nested-item">
  53. <div class="item-actions">
  54. <ArrowUpOutlined title="上移" @click.stop="moveIntroBlockUp(i)" />
  55. <ArrowDownOutlined title="下移" @click.stop="moveIntroBlockDown(i)" />
  56. <CopyOutlined title="复制" @click.stop="copyIntroBlock(i)" />
  57. </div>
  58. <a-form :labelCol="{ span: 6 }" size="small">
  59. <a-form-item label="类型">
  60. <a-input v-model:value="item.type" placeholder="块类型" />
  61. </a-form-item>
  62. <a-form-item label="属性">
  63. <KeyValueEditor v-model:value="item.props" />
  64. </a-form-item>
  65. <a-popconfirm title="确定删除?" @confirm="removeIntroBlock(i)">
  66. <a-button type="link" danger size="small">删除</a-button>
  67. </a-popconfirm>
  68. </a-form>
  69. </div>
  70. <div class="section-footer">
  71. <a-button style="flex: 4;" type="dashed" block size="small" @click="addIntroBlock">+ 添加块</a-button>
  72. <a-button style="flex: 2;" type="dashed" block size="small" @click="pasteIntroBlock">粘贴</a-button>
  73. </div>
  74. </a-collapse-panel>
  75. <a-collapse-panel key="tabs" header="详情 Tab (tabs)">
  76. <div v-for="(tab, i) in tabItems" :key="tabKey(tab, i)" class="nested-item tab-item">
  77. <a-collapse>
  78. <a-collapse-panel :key="i" :header="tabHeader(tab)">
  79. <template #extra>
  80. <div class="item-actions">
  81. <ArrowUpOutlined title="上移" @click.stop="moveTabUp(i)" />
  82. <ArrowDownOutlined title="下移" @click.stop="moveTabDown(i)" />
  83. <CopyOutlined title="复制" @click.stop="copyTab(i)" />
  84. <a-popconfirm title="确认删除该 Tab?" @confirm="removeTab(i)">
  85. <a-button type="text" danger size="small" @click.stop="">
  86. <DeleteOutlined title="删除" />
  87. </a-button>
  88. </a-popconfirm>
  89. </div>
  90. </template>
  91. <a-form :labelCol="{ span: 4 }" size="small">
  92. <a-form-item label="文本">
  93. <a-input v-model:value="tab.text" />
  94. </a-form-item>
  95. <a-form-item label="类型">
  96. <a-select
  97. v-model:value="tab.type"
  98. style="width: 100%"
  99. :options="detailTabTypeOptions"
  100. @change="(v: string) => onTabTypeChange(tab, v)"
  101. />
  102. </a-form-item>
  103. <a-form-item v-if="tab.type === 'images'" label="前缀文字">
  104. <a-input v-model:value="tab.prefix" />
  105. </a-form-item>
  106. <a-form-item label="数据键 key" help="用于根据数据判断当前 TAB 是否显示,如果需要一直显示,可填写 “id“">
  107. <a-input v-model:value="tab.key" placeholder="对应内容数据键" />
  108. </a-form-item>
  109. <a-form-item label="TAB 宽度" help="TAB 宽度,单位:像素(推荐130~300之间)">
  110. <a-input-number v-model:value="tab.width" style="width: 100%" />
  111. </a-form-item>
  112. <a-form-item label="可见">
  113. <a-checkbox v-model:checked="tab.visible" :indeterminate="tab.visible === undefined">
  114. 默认可见
  115. </a-checkbox>
  116. </a-form-item>
  117. <template v-if="tab.type === 'list'">
  118. <a-divider>列表配置</a-divider>
  119. <CommonListPropsEditor :props="ensureListDefine(tab).define.props" />
  120. </template>
  121. <template v-else-if="tab.type === 'nestCategory'">
  122. <a-form-item label="子栏目">
  123. <NestCategoryEditor v-model:categorys="(tab as IHomeCommonCategoryDetailTabItemNestCategoryDefine).categorys" />
  124. </a-form-item>
  125. </template>
  126. <template v-else-if="tab.type === 'rich'">
  127. <a-form-item label="富文本数据键">
  128. <a-input v-model:value="tab.key" placeholder="与上方数据键一致" />
  129. </a-form-item>
  130. </template>
  131. </a-form>
  132. </a-collapse-panel>
  133. </a-collapse>
  134. </div>
  135. <div class="section-footer">
  136. <a-button style="flex: 4;" type="dashed" block size="small" @click="addTab">+ 添加 Tab</a-button>
  137. <a-button style="flex: 2;" type="dashed" block size="small" @click="pasteTab">粘贴</a-button>
  138. </div>
  139. </a-collapse-panel>
  140. </a-collapse>
  141. </div>
  142. </template>
  143. <script setup lang="ts">
  144. import { ref, computed } from 'vue';
  145. import type {
  146. IHomeCommonCategoryDetailDefine,
  147. IHomeCommonCategoryDetailTabItemDefine,
  148. IHomeCommonCategoryDetailTabItemListDefine,
  149. IHomeCommonCategoryDetailTabItemNestCategoryDefine,
  150. } from '../../defines/Details';
  151. import type { IHomeCommonCategoryListDefine } from '../../CommonCategoryDefine';
  152. import CommonListPropsEditor from './CommonListPropsEditor.vue';
  153. import NestCategoryEditor from '../subpart/NestCategoryEditor.vue';
  154. import KeyValueEditor from '../components/KeyValueEditor.vue';
  155. import { ArrowUpOutlined, ArrowDownOutlined, CopyOutlined, DeleteOutlined } from '@ant-design/icons-vue';
  156. import { ArrayUtils } from '@imengyu/imengyu-utils';
  157. const props = defineProps<{
  158. props: IHomeCommonCategoryDetailDefine['props'];
  159. testDetailId: number;
  160. }>();
  161. const emit = defineEmits<{
  162. (e: 'update:testDetailId', value: number): void;
  163. }>();
  164. const activeKeys = ref<string[]>(['introBlockDescs', 'introBlocks', 'tabs']);
  165. const detailTabTypeOptions = [
  166. { value: 'intro', label: '简介' },
  167. { value: 'images', label: '相册图片' },
  168. { value: 'video', label: '视频' },
  169. { value: 'audio', label: '音频' },
  170. { value: 'list', label: '列表' },
  171. { value: 'rich', label: '富文本' },
  172. { value: 'map', label: '地图' },
  173. { value: 'nestCategory', label: '嵌套子分类' },
  174. ];
  175. const introBlockDescsList = computed(() => props.props?.introBlockDescs ?? []);
  176. const introBlocksList = computed(() => props.props?.introBlocks ?? []);
  177. const tabItems = computed(() => (props.props?.tabs || []) as IHomeCommonCategoryDetailTabItemDefine[]);
  178. function tabKey(tab: IHomeCommonCategoryDetailTabItemDefine, i: number) {
  179. return `detail-tab-${i}-${tab.text}-${tab.type}`;
  180. }
  181. function tabHeader(tab: IHomeCommonCategoryDetailTabItemDefine) {
  182. return `${tab.text || 'Tab'} (${tab.type || '?'})`;
  183. }
  184. function getIntroBlockDescs() {
  185. if (!props.props.introBlockDescs) props.props.introBlockDescs = [];
  186. return props.props.introBlockDescs;
  187. }
  188. function addIntroBlockDesc() {
  189. getIntroBlockDescs().push({ label: '', key: '' });
  190. }
  191. function removeIntroBlockDesc(i: number) {
  192. getIntroBlockDescs().splice(i, 1);
  193. }
  194. function moveIntroBlockDescUp(i: number) {
  195. ArrayUtils.upData(getIntroBlockDescs(), i);
  196. }
  197. function moveIntroBlockDescDown(i: number) {
  198. ArrayUtils.downData(getIntroBlockDescs(), i);
  199. }
  200. function copyIntroBlockDesc(i: number) {
  201. const item = getIntroBlockDescs()[i];
  202. uni.setClipboardData({
  203. data: JSON.stringify({ type: 'Copy:DetailIntroBlockDesc', data: item }),
  204. });
  205. uni.showToast({ title: '复制成功', icon: 'success' });
  206. }
  207. async function pasteIntroBlockDesc() {
  208. const res = await uni.getClipboardData();
  209. const data = (res as { data?: string })?.data;
  210. if (typeof data === 'string') {
  211. try {
  212. const json = JSON.parse(data);
  213. if (json.type === 'Copy:DetailIntroBlockDesc') getIntroBlockDescs().push(json.data);
  214. } catch (_) {}
  215. }
  216. }
  217. function getIntroBlocks() {
  218. if (!props.props.introBlocks) props.props.introBlocks = [];
  219. return props.props.introBlocks;
  220. }
  221. function addIntroBlock() {
  222. getIntroBlocks().push({ type: '', props: {} });
  223. }
  224. function removeIntroBlock(i: number) {
  225. getIntroBlocks().splice(i, 1);
  226. }
  227. function moveIntroBlockUp(i: number) {
  228. ArrayUtils.upData(getIntroBlocks(), i);
  229. }
  230. function moveIntroBlockDown(i: number) {
  231. ArrayUtils.downData(getIntroBlocks(), i);
  232. }
  233. function copyIntroBlock(i: number) {
  234. const item = getIntroBlocks()[i];
  235. uni.setClipboardData({
  236. data: JSON.stringify({ type: 'Copy:DetailIntroBlock', data: item }),
  237. });
  238. uni.showToast({ title: '复制成功', icon: 'success' });
  239. }
  240. async function pasteIntroBlock() {
  241. const res = await uni.getClipboardData();
  242. const data = (res as { data?: string })?.data;
  243. if (typeof data === 'string') {
  244. try {
  245. const json = JSON.parse(data);
  246. if (json.type === 'Copy:DetailIntroBlock') getIntroBlocks().push(json.data);
  247. } catch (_) {}
  248. }
  249. }
  250. /** 创建默认列表定义 */
  251. function createDefaultListDefine(): IHomeCommonCategoryListDefine {
  252. return {
  253. type: 'CommonList',
  254. props: {
  255. showTab: true,
  256. showSearch: true,
  257. showTotal: true,
  258. tabs: [],
  259. },
  260. };
  261. }
  262. /** 确保 list 类型 tab 有 define,并返回 define */
  263. function ensureListDefine(tab: IHomeCommonCategoryDetailTabItemDefine): IHomeCommonCategoryDetailTabItemListDefine {
  264. const t = tab as IHomeCommonCategoryDetailTabItemListDefine;
  265. if (!t.define) {
  266. t.define = createDefaultListDefine();
  267. }
  268. return t;
  269. }
  270. function onTabTypeChange(tab: IHomeCommonCategoryDetailTabItemDefine, newType: string) {
  271. if (newType === 'list') {
  272. (tab as IHomeCommonCategoryDetailTabItemListDefine).define = createDefaultListDefine();
  273. } else if (newType === 'nestCategory') {
  274. (tab as IHomeCommonCategoryDetailTabItemNestCategoryDefine).categorys =
  275. (tab as IHomeCommonCategoryDetailTabItemNestCategoryDefine).categorys ?? [];
  276. }
  277. }
  278. function getTabs() {
  279. if (!props.props.tabs) props.props.tabs = [];
  280. return props.props.tabs;
  281. }
  282. function addTab() {
  283. const newTab: IHomeCommonCategoryDetailTabItemDefine = {
  284. text: '新 Tab',
  285. key: '',
  286. type: 'intro',
  287. visible: true,
  288. };
  289. getTabs().push(newTab);
  290. }
  291. function removeTab(i: number) {
  292. getTabs().splice(i, 1);
  293. }
  294. function moveTabUp(i: number) {
  295. ArrayUtils.upData(getTabs(), i);
  296. }
  297. function moveTabDown(i: number) {
  298. ArrayUtils.downData(getTabs(), i);
  299. }
  300. function copyTab(i: number) {
  301. const tab = getTabs()[i];
  302. uni.setClipboardData({
  303. data: JSON.stringify({ type: 'Copy:DetailTabItem', data: tab }),
  304. });
  305. uni.showToast({ title: '复制成功', icon: 'success' });
  306. }
  307. async function pasteTab() {
  308. const res = await uni.getClipboardData();
  309. const data = (res as { data?: string })?.data;
  310. if (typeof data === 'string') {
  311. try {
  312. const json = JSON.parse(data);
  313. if (json.type === 'Copy:DetailTabItem') getTabs().push(json.data);
  314. } catch (_) {}
  315. }
  316. }
  317. </script>
  318. <style scoped>
  319. .detail-props-editor {
  320. font-size: 12px;
  321. }
  322. .props-collapse {
  323. margin-top: 8px;
  324. }
  325. .nested-item {
  326. margin-bottom: 12px;
  327. padding: 8px;
  328. background: #fafafa;
  329. border-radius: 4px;
  330. }
  331. .item-actions {
  332. display: flex;
  333. flex-direction: row;
  334. gap: 8px;
  335. margin-bottom: 8px;
  336. }
  337. .section-footer {
  338. display: flex;
  339. gap: 8px;
  340. }
  341. .section-footer .ant-btn:first-child {
  342. flex: 1;
  343. }
  344. .tab-item :deep(.ant-collapse) {
  345. border: none;
  346. background: transparent;
  347. }
  348. </style>