CardList.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <template>
  2. <div class="p-2">
  3. <div class="bg-white mb-2 p-4">
  4. <BasicForm @register="registerForm" />
  5. </div>
  6. <a-spin :spinning="spinning">
  7. <div class="bg-white p-2">
  8. <slot name="cardTitle"></slot>
  9. <List :grid="{ gutter: 5, xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: grid }" size="small" :data-source="data" :pagination="paginationProp">
  10. <template #header>
  11. <div class="flex justify-start space-x-2">
  12. <slot name="header"></slot>
  13. <Tooltip>
  14. <template #title>
  15. <div class="w-50">每行显示数量</div>
  16. <Slider id="slider" v-bind="sliderProp" v-model:value="grid" @change="sliderChange" />
  17. </template>
  18. <a-button type="primary" style="min-width: 80px">
  19. <TableOutlined />
  20. </a-button>
  21. </Tooltip>
  22. <Tooltip>
  23. <template #title>刷新</template>
  24. <a-button @click="fetch">
  25. <RedoOutlined />
  26. </a-button>
  27. </Tooltip>
  28. </div>
  29. </template>
  30. <template #renderItem="{ item, index }">
  31. <ListItem style="margin-top: 10px">
  32. <Card
  33. class="cardItem"
  34. size="small"
  35. :headStyle="{ textAlign: 'left', fontWeight: '500', background: '#efefef', minHeight: '20px' }"
  36. :bodyStyle="{ display: 'none' }"
  37. >
  38. <template #title>
  39. <em class="aui-tag"><div class="aui-tag-re"></div><div class="aui-tag-ye"></div><div class="aui-tag-bl"></div></em>
  40. <span class="lock-to-right" v-if="item.protectionCode">
  41. <Icon icon="ant-design:lock-filled" :size="15" style="margin: 5px;"/>
  42. </span>
  43. </template>
  44. <!--<template #extra>-->
  45. <!--<Dropdown :trigger="['hover']" :dropMenuList="getDropDownAction(item)" popconfirm>-->
  46. <!--<SettingOutlined />-->
  47. <!--</Dropdown>-->
  48. <!--</template>-->
  49. <template #cover>
  50. <div class="title-div ellipsis">{{ item.name }}</div>
  51. <div class="image-div" @click="handleDesign(item)">
  52. <img :src="getCover(item.coverUrl)" />
  53. <div class="image-mask">
  54. <Icon icon="ant-design:eye-outlined" v-if="params.izTemplate === '1' && !hasAuth()" :size="60" />
  55. <Icon icon="ri:drag-drop-fill" v-else :size="60" />
  56. </div>
  57. </div>
  58. </template>
  59. <template class="ant-card-actions" #actions>
  60. <Tooltip>
  61. <template #title>预览</template>
  62. <EyeOutlined key="view" style="font-size: 20px" @click="handleView(item.id)" />
  63. </Tooltip>
  64. <Dropdown :trigger="['hover']" :dropMenuList="getDropDownAction(item)" popconfirm>
  65. <SettingOutlined key="set" style="font-size: 20px" />
  66. </Dropdown>
  67. </template>
  68. </Card>
  69. </ListItem>
  70. </template>
  71. </List>
  72. </div>
  73. </a-spin>
  74. </div>
  75. </template>
  76. <script lang="ts" setup>
  77. import { computed, onMounted, ref, unref } from 'vue';
  78. import { EyeOutlined, DeleteOutlined, EditOutlined, RedoOutlined, TableOutlined, EllipsisOutlined, SettingOutlined } from '@ant-design/icons-vue';
  79. import { Dropdown } from '/@/components/Dropdown';
  80. import { List, Card, Image, Tooltip, Slider, Popconfirm } from 'ant-design-vue';
  81. import { BasicForm, useForm } from '/@/components/Form';
  82. import { propTypes } from '/@/utils/propTypes';
  83. import { isFunction } from '/@/utils/is';
  84. import { useSlider } from '/@/components/CardList/src/data';
  85. import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
  86. import { useMessage } from '/@/hooks/web/useMessage';
  87. import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
  88. import { usePermission } from '/@/hooks/web/usePermission';
  89. const { hasPermission } = usePermission();
  90. const ListItem = List.Item;
  91. const defCover = 'https://jeecgdev.oss-cn-beijing.aliyuncs.com/temp/designCover_1655434422024.png';
  92. const { createMessage } = useMessage();
  93. const { clipboardRef, copiedRef } = useCopyToClipboard();
  94. const spinning = ref<boolean>(false);
  95. const grid = ref(5);
  96. // 获取slider属性
  97. const sliderProp = computed(() => useSlider(4, 7));
  98. // 组件接收参数
  99. const props = defineProps({
  100. // 请求API的参数
  101. params: propTypes.object.def({}),
  102. //api
  103. api: propTypes.func,
  104. searchFormSchema: propTypes.object.def([]),
  105. });
  106. //暴露内部方法
  107. const emit = defineEmits(['getMethod', 'delete', 'edit', 'view', 'design', 'add', 'copy']);
  108. //数据
  109. const data = ref([]);
  110. // 切换每行个数
  111. // cover图片自适应高度
  112. //修改pageSize并重新请求数据
  113. const height = computed(() => {
  114. return `${200 - grid.value * 8}px`;
  115. });
  116. const width = computed(() => {
  117. let rowNum = grid.value;
  118. let width = rowNum==4?"360px":rowNum==6?"230px":rowNum==7?"190px":"280px";
  119. return width;
  120. });
  121. //表单
  122. const [registerForm, { validate }] = useForm({
  123. schemas: props.searchFormSchema,
  124. labelWidth: 80,
  125. // 操作按钮配置
  126. baseColProps: { span: 6 },
  127. actionColOptions: {
  128. xs: 24,
  129. sm: 12,
  130. md: 12,
  131. lg: 8,
  132. xl: 8,
  133. xxl: 6,
  134. style: { textAlign: 'left' },
  135. },
  136. autoSubmitOnEnter: true,
  137. submitFunc: handleSubmit,
  138. resetFunc: handleReset,
  139. });
  140. //表单提交
  141. async function handleSubmit() {
  142. const data = await validate();
  143. //查询时设置首页
  144. pageNo.value = 1;
  145. await fetch(data);
  146. }
  147. //表单重置
  148. async function handleReset() {
  149. await fetch();
  150. }
  151. async function sliderChange(n) {
  152. pageSize.value = n * 3;
  153. const data = await validate();
  154. fetch(data);
  155. }
  156. // 自动请求并暴露内部方法
  157. onMounted(() => {
  158. fetch();
  159. emit('getMethod', fetch);
  160. });
  161. async function fetch(p = {}) {
  162. spinning.value = true;
  163. const { api, params } = props;
  164. if (api && isFunction(api)) {
  165. const res = await api({ ...params, pageNo: pageNo.value, pageSize: pageSize.value, ...p });
  166. data.value = res.records;
  167. total.value = res.total;
  168. }
  169. spinning.value = false;
  170. }
  171. //分页相关
  172. const pageNo = ref(1);
  173. const pageSize = ref(15);
  174. const total = ref(0);
  175. const paginationProp = ref({
  176. showSizeChanger: false,
  177. showQuickJumper: true,
  178. pageSize,
  179. pageNo,
  180. total,
  181. showTotal: (total) => `总 ${total} 条`,
  182. onChange: pageChange,
  183. onShowSizeChange: pageSizeChange,
  184. });
  185. async function pageChange(p, pz) {
  186. pageNo.value = p;
  187. pageSize.value = pz;
  188. const data = await validate();
  189. fetch(data);
  190. }
  191. async function pageSizeChange(current, size) {
  192. pageSize.value = size;
  193. const data = await validate();
  194. fetch(data);
  195. }
  196. /**
  197. * 获取封面图
  198. */
  199. function getCover(url) {
  200. return url ? getFileAccessHttpUrl(url) : defCover;
  201. }
  202. /**
  203. * 下拉操作栏
  204. */
  205. function getDropDownAction(record) {
  206. //1.如果是模板案例
  207. if(props.params.izTemplate === '1'){
  208. let commonAction = [];
  209. //1.1有权限的用户操作
  210. if(hasAuth()){
  211. commonAction = [
  212. {
  213. text: '编辑',
  214. event: '1',
  215. onClick: handleEdit.bind(null, record),
  216. },
  217. {
  218. text: '复制面板',
  219. event: '2',
  220. onClick: handleCopy.bind(null, record.id),
  221. },{
  222. text: '取消模板',
  223. event: '6',
  224. onClick: handleTemplate.bind(null, record,'0'),
  225. }
  226. ];
  227. }else{
  228. ///1.2没有权限的用户只能复制和预览
  229. commonAction = [
  230. {
  231. text: '复制面板',
  232. event: '2',
  233. onClick: handleCopy.bind(null, record.id),
  234. }
  235. ];
  236. }
  237. return commonAction;
  238. }else{
  239. //非模板案例的tab下的按钮显示
  240. let commonAction = [
  241. {
  242. text: '编辑',
  243. event: '1',
  244. onClick: handleEdit.bind(null, record),
  245. },
  246. {
  247. text: '复制面板',
  248. event: '2',
  249. onClick: handleCopy.bind(null, record.id),
  250. },
  251. {
  252. text: '复制路由',
  253. event: '3',
  254. onClick: handleCopyUrl.bind(null, record.path),
  255. },
  256. ];
  257. //模板按钮的显隐逻辑判断
  258. if(hasAuth()){
  259. if(record.izTemplate == '1'){
  260. commonAction.push({
  261. text: '取消模板',
  262. event: '6',
  263. onClick: handleTemplate.bind(null, record,'0'),
  264. })
  265. }else{
  266. commonAction.push({
  267. text: '收藏模板',
  268. event: '5',
  269. onClick: handleTemplate.bind(null, record,'1'),
  270. })
  271. }
  272. }
  273. //删除按钮的逻辑判断
  274. if(!hasPassword(record)){
  275. commonAction.push({
  276. text: '删除',
  277. event: '4',
  278. popConfirm: {
  279. title: '是否确认删除',
  280. confirm: handleDelete.bind(null, record),
  281. }
  282. })
  283. }else {
  284. commonAction.push({
  285. text: '删除',
  286. event: '4',
  287. onClick: handleDelete.bind(null, record),
  288. })
  289. }
  290. return commonAction;
  291. }
  292. }
  293. /**
  294. * 判断是否有模板操作权限
  295. */
  296. function hasAuth(){
  297. return hasPermission('drag:template:edit')
  298. }
  299. /**
  300. * 是否包含保护码
  301. **/
  302. function hasPassword(record){
  303. return record.protectionCode && record.protectionCode.length > 0;
  304. }
  305. /**
  306. * 复制面板
  307. * @param id
  308. */
  309. function handleCopy(id) {
  310. emit('copy', id);
  311. }
  312. /**
  313. * 复制路由地址
  314. * @param value
  315. */
  316. async function handleCopyUrl(value) {
  317. if (value) {
  318. clipboardRef.value = value;
  319. if (unref(copiedRef)) {
  320. createMessage.success('复制成功!');
  321. }
  322. } else {
  323. createMessage.warning('复制失败,请检查路径!');
  324. }
  325. }
  326. async function handleCreate() {
  327. emit('add');
  328. }
  329. async function handleEdit(record) {
  330. emit('edit', record);
  331. }
  332. async function handleDelete(record) {
  333. emit('delete', record);
  334. }
  335. async function handleView(id) {
  336. emit('view', id);
  337. }
  338. async function handleDesign(record) {
  339. if(props.params.izTemplate === '1' && !hasAuth()){
  340. //模板案例tab下并且没有操作权限时,只能预览
  341. emit('view', record.id);
  342. }else{
  343. emit('design', record);
  344. }
  345. }
  346. async function handleTemplate(record,izTemplate) {
  347. emit('template', record.id,izTemplate);
  348. }
  349. </script>
  350. <style scoped lang="less">
  351. .addItem {
  352. height: 250px;
  353. display: flex;
  354. justify-content: center;
  355. align-items: center;
  356. }
  357. .cardItem {
  358. width: v-bind('width');
  359. text-align: center;
  360. border-radius: 3px;
  361. box-shadow: 0 4px 5px rgb(0 0 0 / 20%);
  362. &:hover {
  363. box-shadow: 0 0 20px 0 #616161;
  364. }
  365. .title-div {
  366. height: 30px;
  367. line-height: 30px;
  368. background: #fff;
  369. font-weight: 500;
  370. font-size: 14px;
  371. border-bottom: 1px solid #efefef;
  372. }
  373. .ellipsis {
  374. display:block;
  375. overflow: hidden;
  376. white-space: nowrap;
  377. text-overflow: ellipsis;
  378. }
  379. .image-div {
  380. img {
  381. width: 100%;
  382. //object-fit: cover;
  383. height: v-bind('height');
  384. }
  385. &:hover {
  386. .image-mask {
  387. opacity: 1;
  388. cursor: default;
  389. }
  390. }
  391. .image-mask {
  392. display: flex;
  393. justify-content: center;
  394. align-items: center;
  395. position: absolute;
  396. width: 100%;
  397. height: v-bind('height');
  398. bottom: 50px;
  399. background: rgba(101, 101, 101, 0.6);
  400. color: #fff;
  401. opacity: 0;
  402. }
  403. }
  404. .aui-tag {
  405. display: flex;
  406. div {
  407. width: 6px;
  408. height: 6px;
  409. background: #333;
  410. border-radius: 100px;
  411. margin: 0 2px;
  412. }
  413. .aui-tag-re {
  414. background: #ff5a52;
  415. }
  416. .aui-tag-ye {
  417. background: #e6c02a;
  418. }
  419. .aui-tag-bl {
  420. background: #53c22c;
  421. }
  422. }
  423. }
  424. /*右上角锁标记*/
  425. .lock-to-right{
  426. z-index: 0;
  427. color: #fff;
  428. position: absolute;
  429. right: 0;
  430. top: 0;
  431. }
  432. .lock-to-right::after {
  433. content: '';
  434. position: absolute;
  435. top: 0;
  436. right: 0;
  437. border-style: solid;
  438. border-width: 0 50px 40px 0;
  439. border-color: transparent #db2828 transparent transparent;
  440. z-index: -1
  441. }
  442. </style>