Commit 1e63198f4ad59695f3310f0b6fed73c23f11cd92

Authored by YunaiV
1 parent 9f7b646f

mp:完善【发布功能】的相关代码

yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java
... ... @@ -40,6 +40,7 @@ public interface ErrorCodeConstants {
40 40 // ========== 公众号发布能力 1006006000============
41 41 ErrorCode FREE_PUBLISH_LIST_FAIL = new ErrorCode(1006006000, "获得已成功发布列表失败,原因:{}");
42 42 ErrorCode FREE_PUBLISH_SUBMIT_FAIL = new ErrorCode(1006006001, "提交发布失败,原因:{}");
  43 + ErrorCode FREE_PUBLISH_DELETE_FAIL = new ErrorCode(1006006001, "删除发布失败,原因:{}");
43 44  
44 45 // ========== 公众号草稿 1006007000============
45 46 ErrorCode DRAFT_LIST_FAIL = new ErrorCode(1006007000, "获得草稿列表失败,原因:{}");
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.java
1 1 package cn.iocoder.yudao.module.mp.controller.admin.news;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5 7 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
6 8 import cn.iocoder.yudao.module.mp.controller.admin.news.vo.MpFreePublishPageReqVO;
  9 +import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO;
7 10 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
  11 +import cn.iocoder.yudao.module.mp.service.material.MpMaterialService;
8 12 import io.swagger.annotations.Api;
9 13 import io.swagger.annotations.ApiImplicitParam;
10 14 import io.swagger.annotations.ApiImplicitParams;
... ... @@ -13,16 +17,21 @@ import me.chanjar.weixin.common.error.WxErrorException;
13 17 import me.chanjar.weixin.mp.api.WxMpService;
14 18 import me.chanjar.weixin.mp.bean.freepublish.WxMpFreePublishItem;
15 19 import me.chanjar.weixin.mp.bean.freepublish.WxMpFreePublishList;
  20 +import org.springframework.security.access.prepost.PreAuthorize;
16 21 import org.springframework.validation.annotation.Validated;
17 22 import org.springframework.web.bind.annotation.*;
18 23  
19 24 import javax.annotation.Resource;
  25 +import java.util.HashSet;
  26 +import java.util.List;
  27 +import java.util.Map;
  28 +import java.util.Set;
20 29  
21 30 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
22 31 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  32 +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
23 33 import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.*;
24 34  
25   -// TODO 芋艿:权限
26 35 @Api(tags = "管理后台 - 公众号发布能力")
27 36 @RestController
28 37 @RequestMapping("/mp/free-publish")
... ... @@ -32,8 +41,12 @@ public class MpFreePublishController {
32 41 @Resource
33 42 private MpServiceFactory mpServiceFactory;
34 43  
  44 + @Resource
  45 + private MpMaterialService mpMaterialService;
  46 +
35 47 @GetMapping("/page")
36 48 @ApiOperation("获得已发布的图文分页")
  49 + @PreAuthorize("@ss.hasPermission('mp:free-publish:query')")
37 50 public CommonResult<PageResult<WxMpFreePublishItem>> getFreePublishPage(MpFreePublishPageReqVO reqVO) {
38 51 // 从公众号查询已发布的图文列表
39 52 WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId());
... ... @@ -44,12 +57,29 @@ public class MpFreePublishController {
44 57 } catch (WxErrorException e) {
45 58 throw exception(FREE_PUBLISH_LIST_FAIL, e.getError().getErrorMsg());
46 59 }
47   - // todo 芋艿:需要查询对应的缩略图,不然前端无法展示
  60 + // 查询对应的图片地址。目的:解决公众号的图片链接无法在我们后台展示
  61 + setFreePublishThumbUrl(publicationRecords.getItems());
48 62  
49 63 // 返回分页
50 64 return success(new PageResult<>(publicationRecords.getItems(), publicationRecords.getTotalCount().longValue()));
51 65 }
52 66  
  67 + private void setFreePublishThumbUrl(List<WxMpFreePublishItem> items) {
  68 + // 1.1 获得 mediaId 数组
  69 + Set<String> mediaIds = new HashSet<>();
  70 + items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> mediaIds.add(newsItem.getThumbMediaId())));
  71 + if (CollUtil.isEmpty(mediaIds)) {
  72 + return;
  73 + }
  74 + // 1.2 批量查询对应的 Media 素材
  75 + Map<String, MpMaterialDO> materials = CollectionUtils.convertMap(mpMaterialService.getMaterialListByMediaId(mediaIds),
  76 + MpMaterialDO::getMediaId);
  77 +
  78 + // 2. 设置回 WxMpFreePublishItem 记录
  79 + items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem ->
  80 + findAndThen(materials, newsItem.getThumbMediaId(), material -> newsItem.setThumbUrl(material.getUrl()))));
  81 + }
  82 +
53 83 @PostMapping("/submit")
54 84 @ApiOperation("发布草稿")
55 85 @ApiImplicitParams({
... ... @@ -58,6 +88,7 @@ public class MpFreePublishController {
58 88 @ApiImplicitParam(name = "mediaId", value = "要发布的草稿的 media_id", required = true,
59 89 example = "2048", dataTypeClass = String.class)
60 90 })
  91 + @PreAuthorize("@ss.hasPermission('mp:free-publish:submit')")
61 92 public CommonResult<String> submitFreePublish(@RequestParam("accountId") Long accountId,
62 93 @RequestParam("mediaId") String mediaId) {
63 94 WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
... ... @@ -69,5 +100,24 @@ public class MpFreePublishController {
69 100 }
70 101 }
71 102  
  103 + @DeleteMapping("/delete")
  104 + @ApiOperation("删除草稿")
  105 + @ApiImplicitParams({
  106 + @ApiImplicitParam(name = "accountId", value = "公众号账号的编号", required = true,
  107 + example = "1024", dataTypeClass = Long.class),
  108 + @ApiImplicitParam(name = "articleId", value = "发布记录的编号", required = true,
  109 + example = "2048", dataTypeClass = String.class)
  110 + })
  111 + @PreAuthorize("@ss.hasPermission('mp:free-publish:delete')")
  112 + public CommonResult<Boolean> deleteFreePublish(@RequestParam("accountId") Long accountId,
  113 + @RequestParam("articleId") String articleId) {
  114 + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
  115 + try {
  116 + mpService.getFreePublishService().deletePushAllArticle(articleId);
  117 + return success(true);
  118 + } catch (WxErrorException e) {
  119 + throw exception(FREE_PUBLISH_DELETE_FAIL, e.getError().getErrorMsg());
  120 + }
  121 + }
72 122  
73 123 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/material/MpMaterialMapper.java
... ... @@ -7,6 +7,9 @@ import cn.iocoder.yudao.module.mp.controller.admin.material.vo.MpMaterialPageReq
7 7 import cn.iocoder.yudao.module.mp.dal.dataobject.material.MpMaterialDO;
8 8 import org.apache.ibatis.annotations.Mapper;
9 9  
  10 +import java.util.Collection;
  11 +import java.util.List;
  12 +
10 13 @Mapper
11 14 public interface MpMaterialMapper extends BaseMapperX<MpMaterialDO> {
12 15  
... ... @@ -21,4 +24,8 @@ public interface MpMaterialMapper extends BaseMapperX&lt;MpMaterialDO&gt; {
21 24 .eqIfPresent(MpMaterialDO::getType, pageReqVO.getType()));
22 25 }
23 26  
  27 + default List<MpMaterialDO> selectListByMediaId(Collection<String> mediaIds) {
  28 + return selectList(MpMaterialDO::getMediaId, mediaIds);
  29 + }
  30 +
24 31 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialService.java
... ... @@ -9,6 +9,8 @@ import me.chanjar.weixin.common.api.WxConsts;
9 9  
10 10 import javax.validation.Valid;
11 11 import java.io.IOException;
  12 +import java.util.Collection;
  13 +import java.util.List;
12 14  
13 15 /**
14 16 * 公众号素材 Service 接口
... ... @@ -55,4 +57,12 @@ public interface MpMaterialService {
55 57 */
56 58 PageResult<MpMaterialDO> getMaterialPage(MpMaterialPageReqVO pageReqVO);
57 59  
  60 + /**
  61 + * 获得素材列表
  62 + *
  63 + * @param mediaIds 素材 mediaId 列表
  64 + * @return 素材列表
  65 + */
  66 + List<MpMaterialDO> getMaterialListByMediaId(Collection<String> mediaIds);
  67 +
58 68 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java
... ... @@ -27,6 +27,8 @@ import org.springframework.validation.annotation.Validated;
27 27 import javax.annotation.Resource;
28 28 import java.io.File;
29 29 import java.io.IOException;
  30 +import java.util.Collection;
  31 +import java.util.List;
30 32  
31 33 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
32 34 import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.MATERIAL_UPLOAD_FAIL;
... ... @@ -146,6 +148,11 @@ public class MpMaterialServiceImpl implements MpMaterialService {
146 148 return mpMaterialMapper.selectPage(pageReqVO);
147 149 }
148 150  
  151 + @Override
  152 + public List<MpMaterialDO> getMaterialListByMediaId(Collection<String> mediaIds) {
  153 + return mpMaterialMapper.selectListByMediaId(mediaIds);
  154 + }
  155 +
149 156 /**
150 157 * 下载微信媒体文件的内容,并上传到文件服务
151 158 *
... ...
yudao-ui-admin/src/api/mp/freePublish.js
... ... @@ -8,3 +8,11 @@ export function getFreePublishPage(query) {
8 8 params: query
9 9 })
10 10 }
  11 +
  12 +// 删除公众号素材
  13 +export function deleteFreePublish(accountId, articleId) {
  14 + return request({
  15 + url: '/mp/free-publish/delete?accountId=' + accountId + '&articleId=' + articleId,
  16 + method: 'delete'
  17 + })
  18 +}
... ...
yudao-ui-admin/src/views/mp/components/wx-material-select/main.vue
1 1 <!--
2 2 - Copyright (C) 2018-2019
3 3 - All rights reserved, Designed By www.joolun.com
  4 + 芋道源码:
  5 + ① 移除 avue 组件,使用 ElementUI 原生组件
4 6 -->
5 7 <template>
6 8 <!-- 类型:图片 -->
... ...
yudao-ui-admin/src/views/mp/freePublish/index.vue 0 → 100644
  1 +<!--
  2 +MIT License
  3 +
  4 +Copyright (c) 2020 www.joolun.com
  5 +
  6 +Permission is hereby granted, free of charge, to any person obtaining a copy
  7 +of this software and associated documentation files (the "Software"), to deal
  8 +in the Software without restriction, including without limitation the rights
  9 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10 +copies of the Software, and to permit persons to whom the Software is
  11 +furnished to do so, subject to the following conditions:
  12 +
  13 +The above copyright notice and this permission notice shall be included in all
  14 +copies or substantial portions of the Software.
  15 +
  16 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 +SOFTWARE.
  23 + 芋道源码:
  24 + ① 优化代码,和项目的代码保持一致
  25 +-->
  26 +<template>
  27 + <div class="app-container">
  28 + <!-- 搜索工作栏 -->
  29 + <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
  30 + <el-form-item label="公众号" prop="accountId">
  31 + <el-select v-model="queryParams.accountId" placeholder="请选择公众号">
  32 + <el-option v-for="item in accounts" :key="parseInt(item.id)" :label="item.name" :value="parseInt(item.id)" />
  33 + </el-select>
  34 + </el-form-item>
  35 + <el-form-item>
  36 + <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
  37 + <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
  38 + </el-form-item>
  39 + </el-form>
  40 +
  41 + <!-- 列表 -->
  42 + <div class="waterfall" v-loading="loading">
  43 + <div v-if="item.content && item.content.newsItem" class="waterfall-item" v-for="item in list"
  44 + :key='item.articleId'>
  45 + <wx-news :articles="item.content.newsItem" />
  46 + <!-- 操作 -->
  47 + <el-row class="ope-row">
  48 + <el-button type="danger" icon="el-icon-delete" circle @click="handleDelete(item)"
  49 + v-hasPermi="['mp:free-publish:delete']" />
  50 + </el-row>
  51 + </div>
  52 + </div>
  53 + <!-- 分页组件 -->
  54 + <div v-if="list.length <=0 && !loading" class="el-table__empty-block">
  55 + <span class="el-table__empty-text">暂无数据</span>
  56 + </div>
  57 + <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
  58 + @pagination="getList"/>
  59 + </div>
  60 +</template>
  61 +
  62 +<script>
  63 +import { getFreePublishPage, deleteFreePublish } from "@/api/mp/freePublish";
  64 +import { getSimpleAccounts } from "@/api/mp/account";
  65 +import WxNews from '@/views/mp/components/wx-news/main.vue';
  66 +
  67 +export default {
  68 + name: 'mpDraft',
  69 + components: {
  70 + WxNews
  71 + },
  72 + data() {
  73 + return {
  74 + // 遮罩层
  75 + loading: false,
  76 + // 显示搜索条件
  77 + showSearch: true,
  78 + // 总条数
  79 + total: 0,
  80 + // 已发表列表
  81 + list: [],
  82 + // 查询参数
  83 + queryParams: {
  84 + total: 0, // 总页数
  85 + currentPage: 1, // 当前页数
  86 + queryParamsSize: 10 // 每页显示多少条
  87 + },
  88 +
  89 + // 公众号账号列表
  90 + accounts: [],
  91 + }
  92 + },
  93 + created() {
  94 + getSimpleAccounts().then(response => {
  95 + this.accounts = response.data;
  96 + // 默认选中第一个
  97 + if (this.accounts.length > 0) {
  98 + this.queryParams.accountId = this.accounts[0].id;
  99 + }
  100 + // 加载数据
  101 + this.getList();
  102 + })
  103 + },
  104 + methods: {
  105 + /** 查询列表 */
  106 + getList() {
  107 + // 如果没有选中公众号账号,则进行提示。
  108 + if (!this.queryParams.accountId) {
  109 + this.$message.error('未选中公众号,无法查询已发表图文')
  110 + return false
  111 + }
  112 +
  113 + this.loading = true;
  114 + getFreePublishPage(this.queryParams).then(response => {
  115 + // 将 thumbUrl 转成 picUrl,保证 wx-news 组件可以预览封面
  116 + response.data.list.forEach(item => {
  117 + const newsItem = item.content.newsItem;
  118 + newsItem.forEach(article => {
  119 + article.picUrl = article.thumbUrl;
  120 + })
  121 + })
  122 + this.list = response.data.list
  123 + this.total = response.data.total
  124 + }).finally(() => {
  125 + this.loading = false
  126 + })
  127 + },
  128 + /** 搜索按钮操作 */
  129 + handleQuery() {
  130 + this.queryParams.pageNo = 1;
  131 + this.getList();
  132 + },
  133 + /** 重置按钮操作 */
  134 + resetQuery() {
  135 + this.resetForm("queryForm");
  136 + // 默认选中第一个
  137 + if (this.accounts.length > 0) {
  138 + this.queryParams.accountId = this.accounts[0].id;
  139 + }
  140 + this.handleQuery();
  141 + },
  142 + /** 删除按钮操作 */
  143 + handleDelete(item){
  144 + const articleId = item.articleId;
  145 + const accountId = this.queryParams.accountId;
  146 + this.$modal.confirm('删除后用户将无法访问此页面,确定删除?').then(function() {
  147 + return deleteFreePublish(accountId, articleId);
  148 + }).then(() => {
  149 + this.getList();
  150 + this.$modal.msgSuccess("删除成功");
  151 + }).catch(() => {});
  152 + },
  153 + }
  154 +}
  155 +</script>
  156 +
  157 +<style lang="scss" scoped>
  158 + .pagination {
  159 + float: right;
  160 + margin-right: 25px;
  161 + }
  162 +
  163 + .add_but {
  164 + padding: 10px;
  165 + }
  166 +
  167 + .ope-row {
  168 + margin-top: 5px;
  169 + text-align: center;
  170 + border-top: 1px solid #eaeaea;
  171 + padding-top: 5px;
  172 + }
  173 +
  174 + .item-name {
  175 + font-size: 12px;
  176 + overflow: hidden;
  177 + text-overflow: ellipsis;
  178 + white-space: nowrap;
  179 + text-align: center;
  180 + }
  181 +
  182 + .el-upload__tip {
  183 + margin-left: 5px;
  184 + }
  185 +
  186 + /*新增图文*/
  187 + .left {
  188 + display: inline-block;
  189 + width: 35%;
  190 + vertical-align: top;
  191 + margin-top: 200px;
  192 + }
  193 +
  194 + .right {
  195 + display: inline-block;
  196 + width: 60%;
  197 + margin-top: -40px;
  198 + }
  199 +
  200 + .avatar-uploader {
  201 + width: 20%;
  202 + display: inline-block;
  203 + }
  204 +
  205 + .avatar-uploader .el-upload {
  206 + border-radius: 6px;
  207 + cursor: pointer;
  208 + position: relative;
  209 + overflow: hidden;
  210 + text-align: unset !important;
  211 + }
  212 +
  213 + .avatar-uploader .el-upload:hover {
  214 + border-color: #165dff;
  215 + }
  216 +
  217 + .avatar-uploader-icon {
  218 + border: 1px solid #d9d9d9;
  219 + font-size: 28px;
  220 + color: #8c939d;
  221 + width: 120px;
  222 + height: 120px;
  223 + line-height: 120px;
  224 + text-align: center;
  225 + }
  226 +
  227 + .avatar {
  228 + width: 230px;
  229 + height: 120px;
  230 + }
  231 +
  232 + .avatar1 {
  233 + width: 120px;
  234 + height: 120px;
  235 + }
  236 +
  237 + .digest {
  238 + width: 60%;
  239 + display: inline-block;
  240 + vertical-align: top;
  241 + }
  242 +
  243 + /*新增图文*/
  244 + /*瀑布流样式*/
  245 + .waterfall {
  246 + width: 100%;
  247 + column-gap: 10px;
  248 + column-count: 5;
  249 + margin: 0 auto;
  250 + }
  251 +
  252 + .waterfall-item {
  253 + padding: 10px;
  254 + margin-bottom: 10px;
  255 + break-inside: avoid;
  256 + border: 1px solid #eaeaea;
  257 + }
  258 +
  259 + p {
  260 + line-height: 30px;
  261 + }
  262 +
  263 + @media (min-width: 992px) and (max-width: 1300px) {
  264 + .waterfall {
  265 + column-count: 3;
  266 + }
  267 + p {
  268 + color: red;
  269 + }
  270 + }
  271 +
  272 + @media (min-width: 768px) and (max-width: 991px) {
  273 + .waterfall {
  274 + column-count: 2;
  275 + }
  276 + p {
  277 + color: orange;
  278 + }
  279 + }
  280 +
  281 + @media (max-width: 767px) {
  282 + .waterfall {
  283 + column-count: 1;
  284 + }
  285 + }
  286 +
  287 + /*瀑布流样式*/
  288 + .news-main {
  289 + background-color: #FFFFFF;
  290 + width: 100%;
  291 + margin: auto;
  292 + height: 120px;
  293 + }
  294 +
  295 + .news-content {
  296 + background-color: #acadae;
  297 + width: 100%;
  298 + height: 120px;
  299 + position: relative;
  300 + }
  301 +
  302 + .news-content-title {
  303 + display: inline-block;
  304 + font-size: 15px;
  305 + color: #FFFFFF;
  306 + position: absolute;
  307 + left: 0px;
  308 + bottom: 0px;
  309 + background-color: black;
  310 + width: 98%;
  311 + padding: 1%;
  312 + opacity: 0.65;
  313 + overflow: hidden;
  314 + text-overflow: ellipsis;
  315 + white-space: nowrap;
  316 + height: 25px;
  317 + }
  318 +
  319 + .news-main-item {
  320 + background-color: #FFFFFF;
  321 + padding: 5px 0px;
  322 + border-top: 1px solid #eaeaea;
  323 + width: 100%;
  324 + margin: auto;
  325 + }
  326 +
  327 + .news-content-item {
  328 + position: relative;
  329 + margin-left: -3px
  330 + }
  331 +
  332 + .news-content-item-title {
  333 + display: inline-block;
  334 + font-size: 12px;
  335 + width: 70%;
  336 + }
  337 +
  338 + .news-content-item-img {
  339 + display: inline-block;
  340 + width: 25%;
  341 + background-color: #acadae
  342 + }
  343 +
  344 + .input-tt {
  345 + padding: 5px;
  346 + }
  347 +
  348 + .activeAddNews {
  349 + border: 5px solid #2bb673;
  350 + }
  351 +
  352 + .news-main-plus {
  353 + width: 280px;
  354 + text-align: center;
  355 + margin: auto;
  356 + height: 50px;
  357 + }
  358 +
  359 + .icon-plus {
  360 + margin: 10px;
  361 + font-size: 25px;
  362 + }
  363 +
  364 + .select-item {
  365 + width: 60%;
  366 + padding: 10px;
  367 + margin: 0 auto 10px auto;
  368 + border: 1px solid #eaeaea;
  369 + }
  370 +
  371 + .father .child {
  372 + display: none;
  373 + text-align: center;
  374 + position: relative;
  375 + bottom: 25px;
  376 + }
  377 +
  378 + .father:hover .child {
  379 + display: block;
  380 + }
  381 +
  382 + .thumb-div {
  383 + display: inline-block;
  384 + width: 30%;
  385 + text-align: center;
  386 + }
  387 +
  388 + .thumb-but {
  389 + margin: 5px;
  390 + }
  391 +
  392 + .material-img {
  393 + width: 100%;
  394 + height: 100%;
  395 + }
  396 +</style>
... ...