Browse Source

seo套餐管理

chenlei1231 6 months ago
parent
commit
a8b85e9b9b

File diff suppressed because it is too large
+ 6230 - 447
pnpm-lock.yaml


+ 238 - 0
src/api/manage/manage.ts

@@ -0,0 +1,238 @@
+import { VueElement } from 'vue';
+import {defHttp} from '/@/utils/http/axios';
+
+const api = {
+  user: '/mock/api/user',
+  role: '/mock/api/role',
+  service: '/mock/api/service',
+  permission: '/mock/api/permission',
+  permissionNoPager: '/mock/api/permission/no-pager'
+}
+
+//post
+export function postAction(url, parameter, timeout) {
+  return defHttp.request({
+    url: url,
+    method: 'post',
+    data: parameter,
+    timeout: timeout ? timeout : 1000 * 120,
+  })
+}
+
+//post
+export function postAction2(url, parameter) {
+  return defHttp.request({
+    url: url,
+    method: 'post',
+    data: parameter,
+    headers: {
+      'Content-Type': 'application/json'
+    },
+    responseType: 'blob'
+  })
+}
+
+//post
+export function postAction3(url, parameter) {
+  return defHttp.request({
+    url: url,
+    method: 'post',
+    data: parameter,
+    timeout: 1000 * 120,
+    headers: {
+      'Content-Type': 'application/json'
+    },
+  })
+}
+
+export function postAction4(url, parameter) {
+  return defHttp.request({
+    url: url,
+    method: 'post',
+    data: parameter,
+    timeout: 1000 * 60 * 3,
+  })
+}
+
+export function postAction5(url, parameter, timeout) {
+  return defHttp.request({
+    url: url,
+    method: 'post',
+    data: parameter,
+    timeout: timeout ? timeout : 1000 * 120,
+    headers: {
+      'Content-Type': 'application/json'
+    },
+  })
+}
+
+//post method= {post | put}
+export function httpAction(url, parameter, method) {
+  return defHttp.request({
+    url: url,
+    method: method,
+    data: parameter
+  })
+}
+
+//put
+export function putAction(url, parameter) {
+  return defHttp.request({
+    url: url,
+    method: 'put',
+    data: parameter
+  })
+}
+
+//get
+export function getAction(url, parameter) {
+  return defHttp.request({
+    url: url,
+    method: 'get',
+    params: parameter,
+    timeout: 1000 * 180,
+  })
+}
+
+export function getAction2(url, parameter) {
+  return axios2({
+    url: url,
+    method: 'get',
+    params: parameter,
+    timeout: 1000 * 180,
+  })
+}
+
+//deleteAction
+export function deleteAction(url, parameter) {
+  return defHttp.request({
+    url: url,
+    method: 'delete',
+    params: parameter
+  })
+}
+
+export function getUserList(parameter) {
+  return defHttp.request({
+    url: api.user,
+    method: 'get',
+    params: parameter
+  })
+}
+
+export function getRoleList(parameter) {
+  return defHttp.request({
+    url: api.role,
+    method: 'get',
+    params: parameter
+  })
+}
+
+export function getServiceList(parameter) {
+  return defHttp.request({
+    url: api.service,
+    method: 'get',
+    params: parameter
+  })
+}
+
+export function getPermissions(parameter) {
+  return defHttp.request({
+    url: api.permissionNoPager,
+    method: 'get',
+    params: parameter
+  })
+}
+
+// id == 0 add     post
+// id != 0 update  put
+export function saveService(parameter) {
+  return defHttp.request({
+    url: api.service,
+    method: parameter.id == 0 ? 'post' : 'put',
+    data: parameter
+  })
+}
+
+/**
+ * 下载文件 用于excel导出
+ * @param url
+ * @param parameter
+ * @returns {*}
+ */
+export function downFile(url, parameter, timeout) {
+  return defHttp.request({
+    url: url,
+    params: parameter,
+    method: 'get',
+    timeout: timeout ? timeout : 1000 * 30,
+    responseType: 'blob'
+  })
+}
+
+/**
+ * 下载文件
+ * @param url 文件路径
+ * @param fileName 文件名
+ * @param parameter
+ * @returns {*}
+ */
+export function downloadFile(url, fileName, parameter) {
+  return downFile(url, parameter).then((data) => {
+    if (!data || data.size === 0) {
+      VueElement.prototype['$message'].warning('文件下载失败')
+      return
+    }
+    if (typeof window.navigator.msSaveBlob !== 'undefined') {
+      window.navigator.msSaveBlob(new Blob([data]), fileName)
+    } else {
+      let url = window.URL.createObjectURL(new Blob([data]))
+      let link = document.createElement('a')
+      link.style.display = 'none'
+      link.href = url
+      link.setAttribute('download', fileName)
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link) //下载完成移除元素
+      window.URL.revokeObjectURL(url) //释放掉blob对象
+    }
+  })
+}
+
+/**
+ * 文件上传 用于富文本上传图片
+ * @param url
+ * @param parameter
+ * @returns {*}
+ */
+export function uploadAction(url, parameter) {
+  return defHttp.request({
+    url: url,
+    data: parameter,
+    method: 'post',
+    headers: {
+      'Content-Type': 'multipart/form-data',  // 文件上传
+    },
+  })
+}
+
+/**
+ * 获取文件服务访问路径
+ * @param avatar
+ * @param subStr
+ * @returns {*}
+ */
+export function getFileAccessHttpUrl(avatar, subStr) {
+  if (!subStr) subStr = 'http'
+  try {
+    if (avatar && avatar.startsWith(subStr)) {
+      return avatar;
+    } else {
+      if (avatar && avatar.length > 0 && avatar.indexOf('[') == -1) {
+        return window._CONFIG['staticDomainURL'] + "/" + avatar;
+      }
+    }
+  } catch (err) {
+    return;
+  }
+}

+ 89 - 0
src/assets/less/common.less

@@ -0,0 +1,89 @@
+
+/*列表上方操作按钮区域*/
+.ant-card-body .table-operator {
+  margin-bottom: 8px;
+}
+/** Button按钮间距 */
+.table-operator .ant-btn {
+  margin: 0 8px 8px 0;
+}
+.table-operator .ant-btn-group .ant-btn {
+  margin: 0;
+}
+
+.table-operator .ant-btn-group .ant-btn:last-child {
+  margin: 0 8px 8px 0;
+}
+/*列表td的padding设置 可以控制列表大小*/
+.ant-table-tbody .ant-table-row td {
+  padding-top: 15px;
+  padding-bottom: 15px;
+}
+
+/*列表页面弹出modal*/
+.ant-modal-cust-warp {
+  height: 100%
+}
+
+/*弹出modal Y轴滚动条*/
+.ant-modal-cust-warp .ant-modal-body {
+  height: calc(100% - 110px) !important;
+  overflow-y: auto
+}
+
+/*弹出modal 先有content后有body 故滚动条控制在body上*/
+.ant-modal-cust-warp .ant-modal-content {
+  height: 90% !important;
+  overflow-y: hidden
+}
+/*列表中有图片的加这个样式 参考用户管理*/
+.anty-img-wrap {
+  height: 25px;
+  position: relative;
+}
+.anty-img-wrap > img {
+  max-height: 100%;
+}
+/*列表中范围查询样式*/
+.query-group-cust{width: calc(50% - 10px)}
+.query-group-split-cust:before{content:"~";width: 20px;display: inline-block;text-align: center}
+
+
+/*erp风格子表外框padding设置*/
+.ant-card-wider-padding.cust-erp-sub-tab>.ant-card-body{padding:5px 12px}
+
+/* 内嵌子表背景颜色 */
+.j-inner-table-wrapper /deep/ .ant-table-expanded-row .ant-table-wrapper .ant-table-tbody .ant-table-row {
+  background-color: #FFFFFF;
+}
+
+/**隐藏样式-modal确定按钮 */
+.jee-hidden{display: none}
+
+
+.ant-table-tbody > tr > td {
+  padding: 8px 8px !important
+}
+
+.ant-table{
+  line-height: 1;
+}
+.ant-table .ant-table-row{
+  height: 45px!important
+}
+
+.ant-table  .ant-table-thead tr{
+  height: 43px!important
+}
+
+.ant-modal-body{
+  padding: 24px;
+}
+
+.ant-btn-link {
+  color: #544BEB!important;
+  background-color: transparent!important;
+  border-color: transparent!important;
+  box-shadow: none!important;
+  height: auto!important;
+}

+ 72 - 0
src/views/adweb/seo/SeoMarketPlan.api.ts

@@ -0,0 +1,72 @@
+import { defHttp } from '/@/utils/http/axios';
+import { useMessage } from '/@/hooks/web/useMessage';
+
+const { createConfirm } = useMessage();
+
+enum Api {
+  list = '/adweb/seoMarketPlan/list',
+  save = '/adweb/seoMarketPlan/add',
+  edit = '/adweb/seoMarketPlan/edit',
+  deleteOne = '/adweb/seoMarketPlan/delete',
+  deleteBatch = '/adweb/seoMarketPlan/deleteBatch',
+  importExcel = '/adweb/seoMarketPlan/importExcel',
+  exportXls = '/adweb/seoMarketPlan/exportXls',
+}
+
+/**
+ * 导出api
+ * @param params
+ */
+export const getExportUrl = Api.exportXls;
+
+/**
+ * 导入api
+ */
+export const getImportUrl = Api.importExcel;
+
+/**
+ * 列表接口
+ * @param params
+ */
+export const list = (params) => defHttp.get({ url: Api.list, params });
+
+/**
+ * 删除单个
+ * @param params
+ * @param handleSuccess
+ */
+export const deleteOne = (params, handleSuccess) => {
+  return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
+    handleSuccess();
+  });
+};
+
+/**
+ * 批量删除
+ * @param params
+ * @param handleSuccess
+ */
+export const batchDelete = (params, handleSuccess) => {
+  createConfirm({
+    iconType: 'warning',
+    title: '确认删除',
+    content: '是否删除选中数据',
+    okText: '确认',
+    cancelText: '取消',
+    onOk: () => {
+      return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
+        handleSuccess();
+      });
+    },
+  });
+};
+
+/**
+ * 保存或者更新
+ * @param params
+ * @param isUpdate
+ */
+export const saveOrUpdate = (params, isUpdate) => {
+  const url = isUpdate ? Api.edit : Api.save;
+  return defHttp.post({ url: url, params }, { isTransformResponse: false });
+};

+ 61 - 0
src/views/adweb/seo/SeoMarketPlan.data.ts

@@ -0,0 +1,61 @@
+import {BasicColumn} from '/@/components/Table';
+//列表数据
+export const columns: BasicColumn[] = [
+  {
+    title: '套餐类型',
+    align: 'left',
+    dataIndex: 'marketType_dictText',
+  },
+  {
+    title: '套餐名称',
+    align: 'left',
+    dataIndex: 'planName',
+  },
+  {
+    title: '关键词数量',
+    align: 'center',
+    dataIndex: 'keywordCount',
+    customRender: function({text, record})  {
+      if (record.marketType == 'SEO') {
+        return text + '个'
+      } else if (record.marketType == 'ARTICLE') {
+        return text + '篇'
+      } else if (record.marketType == 'PSEUDOORIGINAL') {
+        return text + '次'
+      }else if (record.marketType == 'STATIONCONSTRUCTION') {
+        return text + '个'
+      }
+    }
+  },
+  {
+    title: '关键词达标目标',
+    align: 'center',
+    dataIndex: 'target',
+  },
+  {
+    title: '服务时间',
+    align: 'center',
+    dataIndex: 'serviceTime',
+    customRender: function ({text}) {
+      return text + '月'
+    }
+  },
+  {
+    title: '价格',
+    align: 'center',
+    dataIndex: 'price',
+    customRender: function ({text}) {
+      return text + '元'
+    }
+  },
+];
+
+// 高级查询数据
+export const superQuerySchema = {
+  marketType: {title: '套餐类型', order: 0, view: 'list', type: 'string'},
+  planName: {title: '套餐名称', order: 1, view: 'text', type: 'string'},
+  keywordCount: {title: '关键词数量', order: 2, view: 'number', type: 'number'},
+  target: {title: '关键词达标目标', order: 3, view: 'number', type: 'number'},
+  serviceTime: {title: '服务时间', order: 4, view: 'number', type: 'number'},
+  price: {title: '价格', order: 5, view: 'number', type: 'number'},
+};

+ 304 - 0
src/views/adweb/seo/SeoMarketPlanList.vue

@@ -0,0 +1,304 @@
+<template>
+  <div class="p-2">
+    <!--查询区域-->
+    <div class="jeecg-basic-table-form-container">
+      <a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
+        <a-row :gutter="24">
+          <a-col :lg="6">
+            <a-form-item name="marketType">
+              <template #label><span title="套餐类型">套餐类型</span></template>
+              <j-dict-select-tag v-model:value="queryParam.marketType" dictCode="dict_market_type" placeholder="请选择套餐类型" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="6">
+            <a-form-item name="planName">
+              <template #label><span title="套餐名称">套餐名称</span></template>
+              <a-input placeholder="请输入套餐名称" v-model:value="queryParam.planName" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :xl="6" :lg="7" :md="8" :sm="24">
+            <span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
+              <a-col :lg="6">
+                <a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
+                <a-button ghost type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
+                <a-button ghost @click="handleAdd" type="primary" preIcon="ant-design:plus-outlined" style="margin-left: 8px">添加套餐</a-button>
+              </a-col>
+            </span>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+    <!--引用表格-->
+    <BasicTable @register="registerTable" :rowSelection="rowSelection">
+      <!--插槽:table标题-->
+      <template #tableTitle>
+<!--        <a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增 </a-button>-->
+        <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出 </a-button>
+<!--        <j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls"> 导入 </j-upload-button>-->
+        <a-dropdown v-if="selectedRowKeys.length > 0">
+          <template #overlay>
+            <a-menu>
+              <a-menu-item key="1" @click="batchHandleDelete">
+                <Icon icon="ant-design:delete-outlined" />
+                删除
+              </a-menu-item>
+            </a-menu>
+          </template>
+          <a-button
+            >批量操作
+            <Icon icon="mdi:chevron-down" />
+          </a-button>
+        </a-dropdown>
+        <!-- 高级查询 -->
+        <super-query :config="superQueryConfig" @search="handleSuperQuery" />
+      </template>
+      <!--操作栏-->
+      <template #action="{ record }">
+        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
+      </template>
+
+      <template #bodyCell="{ column, record, index, text }">
+
+<!--       自定义列 -->
+        <template v-if="column.key == 'marketType_dictText'">
+          <template v-if="record.marketType == 'SEO'">
+            <a-tag color="green">
+              {{ text }}
+            </a-tag>
+          </template>
+          <template v-else-if="record.marketType == 'PSEUDOORIGINAL'">
+            <a-tag color="cyan">
+              {{ text }}
+            </a-tag>
+          </template>
+          <template v-else>
+            <a-tag color="blue">
+              {{ text }}
+            </a-tag>
+          </template>
+        </template>
+
+        <template v-if="column.key == 'target'">
+          <a-tag v-if="text != null && text != ''" color="#f50">TOP {{ text }}</a-tag>
+          <span v-else>-</span>
+        </template>
+
+      </template>
+    </BasicTable>
+    <!-- 表单区域 -->
+    <SeoMarketPlanModal ref="registerModal" @success="handleSuccess" />
+    <IntegrityCheckingDrawer ref="integrityCheckingDrawer" />
+  </div>
+</template>
+
+<script lang="ts" name="adweb-seoMarketPlan" setup>
+  import { reactive, ref } from 'vue';
+  import { BasicTable, TableAction } from '/@/components/Table';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import { columns, superQuerySchema } from './SeoMarketPlan.data';
+  import { batchDelete, deleteOne, getExportUrl, getImportUrl, list } from './SeoMarketPlan.api';
+  import SeoMarketPlanModal from './components/SeoMarketPlanModal.vue';
+  import IntegrityCheckingDrawer from './components/IntegrityCheckingDrawer.vue';
+  import { useUserStore } from '/@/store/modules/user';
+  // import JSearchSelect from '/@/components/Form/src/jeecg/components/JSearchSelect.vue';
+  import JDictSelectTag from "@/components/Form/src/jeecg/components/JDictSelectTag.vue";
+  const formRef = ref();
+  const queryParam = reactive<any>({});
+  // const toggleSearchStatus = ref<boolean>(false);
+  const registerModal = ref();
+  const integrityCheckingDrawer = ref();
+
+  const userStore = useUserStore();
+  //注册table数据
+  const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
+    tableProps: {
+      title: '网站营销方案套餐表',
+      api: list,
+      columns,
+      canResize: false,
+      useSearchForm: false,
+      actionColumn: {
+        width: 200,
+        fixed: 'right',
+      },
+      pagination: {
+        pageSize: 30,
+      },
+      beforeFetch: (params) => {
+        return Object.assign(params, queryParam);
+      },
+    },
+    exportConfig: {
+      name: '网站营销方案套餐表',
+      url: getExportUrl,
+      params: queryParam,
+    },
+    importConfig: {
+      url: getImportUrl,
+      success: handleSuccess,
+    },
+  });
+
+  const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
+
+  const labelCol = reactive({
+    xs: 24,
+    sm: 4,
+    xl: 6,
+    xxl: 4,
+  });
+
+  const wrapperCol = reactive({
+    xs: 24,
+    sm: 20,
+  });
+
+  // 高级查询配置
+  const superQueryConfig = reactive(superQuerySchema);
+
+  /**
+   * 高级查询事件
+   */
+  function handleSuperQuery(params) {
+    Object.keys(params).map((k) => {
+      queryParam[k] = params[k];
+    });
+    searchQuery();
+  }
+
+  /**
+   * 新增事件
+   */
+  function handleAdd() {
+    registerModal.value.disableSubmit = false;
+    registerModal.value.add();
+  }
+
+  /**
+   * 编辑事件
+   */
+  function handleEdit(record: Recordable) {
+    registerModal.value.disableSubmit = false;
+    registerModal.value.edit(record);
+  }
+
+  /**
+   * 物料检测事件
+   */
+  function integrityChecking(record: Recordable) {
+    integrityCheckingDrawer.value.showDrawer(record.id)
+  }
+  /**
+   * 详情
+   */
+  function handleDetail(record: Recordable) {
+    registerModal.value.disableSubmit = true;
+    registerModal.value.edit(record);
+  }
+
+  /**
+   * 删除事件
+   */
+  async function handleDelete(record) {
+    await deleteOne({ id: record.id }, handleSuccess);
+  }
+
+  /**
+   * 批量删除事件
+   */
+  async function batchHandleDelete() {
+    await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
+  }
+
+  /**
+   * 成功回调
+   */
+  function handleSuccess() {
+    (selectedRowKeys.value = []) && reload();
+  }
+
+  /**
+   * 操作栏
+   */
+  function getTableAction(record) {
+    return [
+      {
+        label: '编辑',
+        onClick: handleEdit.bind(null, record),
+      },
+      {
+        label: '检测指标',
+        onclick: integrityChecking.bind(null, record),
+      }
+    ];
+  }
+
+  /**
+   * 下拉操作栏
+   */
+  function getDropDownAction(record) {
+    return [
+      {
+        label: '详情',
+        onClick: handleDetail.bind(null, record),
+      },
+      {
+        label: '删除',
+        popConfirm: {
+          title: '是否确认删除',
+          confirm: handleDelete.bind(null, record),
+          placement: 'topLeft',
+        },
+      },
+    ];
+  }
+
+  /**
+   * 查询
+   */
+  function searchQuery() {
+    reload();
+  }
+
+  /**
+   * 重置
+   */
+  function searchReset() {
+    formRef.value.resetFields();
+    selectedRowKeys.value = [];
+    //刷新数据
+    reload();
+  }
+</script>
+
+<style lang="less" scoped>
+  .jeecg-basic-table-form-container {
+    padding: 0;
+
+    .table-page-search-submitButtons {
+      display: block;
+      margin-bottom: 24px;
+      white-space: nowrap;
+    }
+
+    .query-group-cust {
+      min-width: 100px !important;
+    }
+
+    .query-group-split-cust {
+      width: 30px;
+      display: inline-block;
+      text-align: center;
+    }
+
+    .ant-form-item:not(.ant-form-item-with-help) {
+      margin-bottom: 16px;
+      height: 32px;
+    }
+
+    :deep(.ant-picker),
+    :deep(.ant-input-number) {
+      width: 100%;
+    }
+  }
+</style>

+ 328 - 0
src/views/adweb/seo/components/IntegrityCheckingDrawer.vue

@@ -0,0 +1,328 @@
+<template>
+
+  <a-drawer
+    title="物料检测标准"
+    :width="1200"
+    :visible="visible"
+    :body-style="{ paddingBottom: '24px' }"
+    @close="closeDraw"
+  >
+    <a-row :gutter="16" style="margin-bottom: 20px">
+      <a-col :span="3">
+        <a-button type="primary" @click="submitAll" :loading="loading" :disabled="submitDisable">
+          保存
+        </a-button>
+      </a-col>
+    </a-row>
+    <a-table :columns="columns" :data-source="data" :pagination="false">
+      <template
+        v-for="col in ['weight']"
+        :slot="col"
+        slot-scope="text, record, index"
+        :key="col"
+      >
+        <div>
+          <a-input-number
+            v-if="record.editable"
+            style="margin: -5px 0"
+            :value="text"
+            :autoFocus="true"
+            @change="e => handleChange(e, record.key, col)"
+            :min="0"
+            :step="0.1"
+            :formatter="(value)=>{
+      let reg = /^(-)*(\d+)\.(\d).*$/;
+      return `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',').replace(reg,'$1$2.$3');
+    }"
+            :parser="(value)=>{
+      let reg = /^(-)*(\d+)\.(\d).*$/;
+      return value.replace(/\s?|(,*)/g, '').replace(reg,'$1$2.$3');
+    }"
+          />
+          <template v-else>
+            {{ text }}
+          </template>
+        </div>
+      </template>
+      <template
+        v-for="col in ['limitMin']"
+        :slot="col"
+        slot-scope="text, record, index"
+        :key="col"
+      >
+        <div>
+          <a-input-number
+            v-if="record.editable"
+            style="margin: -5px 0"
+            :value="text"
+            @change="e => handleChange(e, record.key, col)"
+            :min="0"
+            :step="1"
+            :autoFocus="true"
+            :formatter="(value)=>{
+              if (typeof value === 'string') {
+                return !isNaN(Number(value)) ? value.replace(/^(0+)|[^\d]/g, '') : ''
+              }else if (typeof value === 'number') {
+                return !isNaN(value) ? String(value).replace(/^(0+)|[^\d]/g, '') : ''}
+              else {
+                return ''
+              }}"/>
+          <template v-else>
+            {{ text }}
+          </template>
+        </div>
+      </template>
+
+      <template slot="required" slot-scope="text, record">
+        <a-switch checked-children="是" un-checked-children="否" :autoFocus="true"
+                  :checked="record.required === 0" @change="changeRequired(record)"
+                  :loading="changeLoading"/>
+      </template>
+
+      <template slot="operation" slot-scope="text, record, index">
+        <div class="editable-row-operations">
+          <span v-if="record.editable">
+            <a @click="() => save(record.key)">保存</a>
+            <a-divider type="vertical"/>
+            <a-popconfirm title="确定取消吗?" @confirm="() => cancel(record.key)">
+              <a>取消</a>
+            </a-popconfirm>
+          </span>
+          <span v-else>
+            <a :disabled="editingKey !== ''" @click="() => edit(record.key)">编辑</a>
+          </span>
+        </div>
+      </template>
+      <template slot="sort" slot-scope="text, record, index">
+        <a-tag color="pink" @click="up(index)" style="cursor: pointer"
+               :class="index == 0 ? 'disabled' : '' ">
+          <a-icon type="arrow-up"/>
+        </a-tag>
+        <a-tag color="blue" @click="down(index)" style="cursor: pointer"
+               :class="index === data.length-1 ? 'disabled' : '' ">
+          <a-icon type="arrow-down"/>
+        </a-tag>
+      </template>
+    </a-table>
+
+
+  </a-drawer>
+</template>
+
+<script>
+
+import { getAction, postAction } from '/@/api/manage/manage'
+import '/@/assets/less/common.less';
+
+export default {
+  name: 'IntegrityCheckingDrawer',
+
+  data() {
+    return {
+      visible: false,
+      columns: [
+        {
+          title: '物料名称',
+          dataIndex: 'materialLabel',
+          align: 'center'
+        },
+        {
+          title: '最低限制',
+          dataIndex: 'limitMin',
+          align: 'center',
+          scopedSlots: {customRender: 'limitMin'}
+        },
+        {
+          title: '评分占比',
+          dataIndex: 'weight',
+          align: 'center',
+          scopedSlots: {customRender: 'weight'}
+        },
+        {
+          title: '是否必填',
+          dataIndex: 'required',
+          align: 'center',
+          scopedSlots: {customRender: 'required'}
+        },
+        {
+          title: '排序',
+          dataIndex: 'sort',
+          align: 'center',
+          scopedSlots: {customRender: 'sort'}
+        },
+        {
+          title: '操作',
+          dataIndex: 'operation',
+          align: 'center',
+          scopedSlots: {customRender: 'operation'}
+        }
+      ],
+      data: [],
+      cacheData: [],
+      editingKey: '',
+      sortChanged: false, // 判断排序是否改变,排序改变则在关闭的时候进行统一保存
+      loading: false,
+      loading2: false,
+      id: '',
+      changeLoading: false,
+      submitDisable: false,
+    }
+  },
+  methods: {
+    submitAll() {
+      let d = this.data
+      let weight = 0
+      for (let i in d) {
+        weight += parseFloat(d[i].weight)
+      }
+      if (parseFloat(weight) !== 100) {
+        this.$message.error('权重之和应为100')
+        return
+      }
+      this.loading = true
+      postAction('/serp/seoMarketPlan/saveTemplate', {
+        checkList: JSON.stringify(d),
+        planId: this.id
+      }).then((res) => {
+        this.loading = false
+        if (res.code == 200) {
+          this.$message.success('保存成功')
+          this.showDrawer(this.id)
+        } else {
+          this.$message.error(res.message)
+        }
+      }).catch(e => {
+        this.$message.error('保存数据失败!')
+      })
+    },
+    showDrawer(id) {
+      let that = this
+      that.id = id
+      getAction('/serp/seoMarketPlan/integrityChecking?planId=' + id).then((res) => {
+        if (res.success) {
+          that.data = res.result
+          for (let i = 0; i < that.data.length; i++) {
+            let item = that.data[i]
+            item.key = item.id
+          }
+          that.cacheData = that.data.map(item => ({...item}))
+        }
+      }).catch(e => {
+        this.$message.warning('获取数据失败!')
+      })
+      this.visible = true
+    },
+    closeDraw() {
+      let that = this
+      that.visible = false
+      that.data = []
+      that.cacheData = []
+      that.sortChanged = false
+      that.record = {}
+    },
+    handleChange(value, key, column) {
+      const newData = [...this.data]
+      const target = newData.filter(item => key === item.key)[0]
+      if (target) {
+        target[column] = value == null ? '' : value
+        this.data = newData
+      }
+    },
+    edit(key) {
+      const newData = [...this.data]
+      const target = newData.filter(item => key === item.key)[0]
+      this.editingKey = key
+      if (target) {
+        target.editable = true
+        this.data = newData
+      }
+      this.submitDisable = true
+    },
+    save(key) {
+      const newData = [...this.data]
+      const newCacheData = [...this.cacheData]
+      const target = newData.filter(item => key === item.key)[0]
+      const targetCache = newCacheData.filter(item => key === item.key)[0]
+      if (target && targetCache) {
+        delete target.editable
+        this.data = newData
+        Object.assign(targetCache, target)
+        this.cacheData = newCacheData
+      }
+      this.editingKey = ''
+      this.submitDisable = false
+    },
+    cancel(key) {
+      const newData = [...this.data]
+      const target = newData.filter(item => key === item.key)[0]
+      this.editingKey = ''
+      if (target) {
+        Object.assign(target, this.cacheData.filter(item => key === item.key)[0])
+        delete target.editable
+        this.data = newData
+      }
+      this.submitDisable = false
+    },
+    up(index) {
+      let that = this
+      if (that.editingKey !== '') {
+        that.$message.warn('请先完成编辑!')
+        return
+      }
+      if (index === 0) {
+        return
+      }
+
+      that.sortChanged = true
+
+      // 修改展示顺序
+      let data2 = that.data
+      let temp1 = data2[index]
+      data2[index] = data2[index - 1]
+      data2[index - 1] = temp1
+      that.cacheData = data2.map(item => ({...item}))
+      that.data = that.cacheData
+    },
+    down(index) {
+      let that = this
+      if (that.editingKey !== '') {
+        that.$message.warn('请先完成编辑!')
+        return
+      }
+      if (index === that.data.length - 1) {
+        return
+      }
+      that.sortChanged = true
+      // 修改展示顺序
+      let data2 = that.data
+      let temp1 = data2[index]
+      data2[index] = data2[index + 1]
+      data2[index + 1] = temp1
+      that.cacheData = data2.map(item => ({...item}))
+      that.data = that.cacheData
+    },
+
+    changeRequired(record) {
+      let that = this
+      that.changeLoading = true
+      postAction('/materialcollect/changeRequired?id=' + record.id + '&planId=' + record.planId).then((res) => {
+        if (res.success) {
+          that.$message.success(res.result)
+          that.showDrawer(that.id)
+        } else {
+          that.$message.error(res.message)
+        }
+        that.changeLoading = false
+      })
+    },
+
+
+  }
+}
+</script>
+<style lang="less" scoped>
+.ant-tag.disabled {
+  cursor: no-drop !important;
+  opacity: 0.3;
+}
+</style>

+ 171 - 0
src/views/adweb/seo/components/SeoMarketPlanForm.vue

@@ -0,0 +1,171 @@
+<template>
+  <a-spin :spinning="confirmLoading">
+    <JFormContainer :disabled="disabled">
+      <template #detail>
+        <a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol">
+          <a-row>
+            <a-col :span="24">
+              <a-form-item label="套餐类型" v-bind="validateInfos.marketType">
+                <j-dict-select-tag v-model:value="formData.marketType" dictCode="dict_market_type" placeholder="请选择套餐类型" allow-clear />
+              </a-form-item>
+            </a-col>
+            <a-col :span="24">
+              <a-form-item label="套餐名称" v-bind="validateInfos.planName">
+                <a-input v-model:value="formData.planName" placeholder="请输入套餐名称" allow-clear />
+              </a-form-item>
+            </a-col>
+            <a-col :span="24">
+              <a-form-item label="关键词数量" v-bind="validateInfos.keywordCount">
+                <a-input-number v-model:value="formData.keywordCount" placeholder="请输入关键词数量" style="width: 100%" />
+              </a-form-item>
+            </a-col>
+            <a-col :span="24">
+              <a-form-item label="关键词达标目标" v-bind="validateInfos.target">
+                <a-input-number v-model:value="formData.target" placeholder="请输入关键词达标目标" style="width: 100%" />
+              </a-form-item>
+            </a-col>
+            <a-col :span="24">
+              <a-form-item label="服务时间" v-bind="validateInfos.serviceTime">
+                <a-input-number v-model:value="formData.serviceTime" placeholder="请输入服务时间" style="width: 100%" />
+              </a-form-item>
+            </a-col>
+            <a-col :span="24">
+              <a-form-item label="价格" v-bind="validateInfos.price">
+                <a-input-number v-model:value="formData.price" placeholder="请输入价格" style="width: 100%" />
+              </a-form-item>
+            </a-col>
+          </a-row>
+        </a-form>
+      </template>
+    </JFormContainer>
+  </a-spin>
+</template>
+
+<script lang="ts" setup>
+  import { ref, reactive, defineExpose, nextTick, defineProps, computed, onMounted } from 'vue';
+  import { defHttp } from '/@/utils/http/axios';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue';
+  import { getValueType } from '/@/utils';
+  import { saveOrUpdate } from '../SeoMarketPlan.api';
+  import { Form } from 'ant-design-vue';
+  import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue';
+
+  const props = defineProps({
+    formDisabled: { type: Boolean, default: false },
+    formData: { type: Object, default: () => ({}) },
+    formBpm: { type: Boolean, default: true },
+  });
+  const formRef = ref();
+  const useForm = Form.useForm;
+  const emit = defineEmits(['register', 'ok']);
+  const formData = reactive<Record<string, any>>({
+    id: '',
+    marketType: '',
+    planName: '',
+    keywordCount: undefined,
+    target: undefined,
+    serviceTime: undefined,
+    price: undefined,
+  });
+  const { createMessage } = useMessage();
+  const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } });
+  const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } });
+  const confirmLoading = ref<boolean>(false);
+  //表单验证
+  const validatorRules = reactive({
+    marketType: [{ required: true, message: '请输入套餐类型!' }],
+    planName: [{ required: true, message: '请输入套餐名称!' }],
+    keywordCount: [{ required: true, message: '请输入关键词数量!' }],
+    target: [{ required: true, message: '请输入关键词达标目标!' }],
+    serviceTime: [{ required: true, message: '请输入服务时间!' }],
+    price: [{ required: true, message: '请输入价格!' }],
+  });
+  const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
+
+  // 表单禁用
+  const disabled = computed(() => {
+    if (props.formBpm === true) {
+      if (props.formData.disabled === false) {
+        return false;
+      } else {
+        return true;
+      }
+    }
+    return props.formDisabled;
+  });
+
+  /**
+   * 新增
+   */
+  function add() {
+    edit({});
+  }
+
+  /**
+   * 编辑
+   */
+  function edit(record) {
+    nextTick(() => {
+      resetFields();
+      const tmpData = {};
+      Object.keys(formData).forEach((key) => {
+        if (record.hasOwnProperty(key)) {
+          tmpData[key] = record[key];
+        }
+      });
+      //赋值
+      Object.assign(formData, tmpData);
+    });
+  }
+
+  /**
+   * 提交数据
+   */
+  async function submitForm() {
+    // 触发表单验证
+    await validate();
+    confirmLoading.value = true;
+    const isUpdate = ref<boolean>(false);
+    //时间格式化
+    let model = formData;
+    if (model.id) {
+      isUpdate.value = true;
+    }
+    //循环数据
+    for (let data in model) {
+      //如果该数据是数组并且是字符串类型
+      if (model[data] instanceof Array) {
+        let valueType = getValueType(formRef.value.getProps, data);
+        //如果是字符串类型的需要变成以逗号分割的字符串
+        if (valueType === 'string') {
+          model[data] = model[data].join(',');
+        }
+      }
+    }
+    await saveOrUpdate(model, isUpdate.value)
+      .then((res) => {
+        if (res.success) {
+          createMessage.success(res.message);
+          emit('ok');
+        } else {
+          createMessage.warning(res.message);
+        }
+      })
+      .finally(() => {
+        confirmLoading.value = false;
+      });
+  }
+
+  defineExpose({
+    add,
+    edit,
+    submitForm,
+  });
+</script>
+
+<style lang="less" scoped>
+  .antd-modal-form {
+    padding: 14px;
+  }
+</style>

+ 85 - 0
src/views/adweb/seo/components/SeoMarketPlanModal.vue

@@ -0,0 +1,85 @@
+<template>
+  <j-modal
+    :title="title"
+    :width="width"
+    :visible="visible"
+    @ok="handleOk"
+    :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }"
+    @cancel="handleCancel"
+    cancelText="关闭"
+  >
+    <SeoMarketPlanForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false" />
+  </j-modal>
+</template>
+
+<script lang="ts" setup>
+  import { ref, nextTick, defineExpose } from 'vue';
+  import SeoMarketPlanForm from './SeoMarketPlanForm.vue';
+  import JModal from '/@/components/Modal/src/JModal/JModal.vue';
+
+  const title = ref<string>('');
+  const width = ref<number>(800);
+  const visible = ref<boolean>(false);
+  const disableSubmit = ref<boolean>(false);
+  const registerForm = ref();
+  const emit = defineEmits(['register', 'success']);
+
+  /**
+   * 新增
+   */
+  function add() {
+    title.value = '新增';
+    visible.value = true;
+    nextTick(() => {
+      registerForm.value.add();
+    });
+  }
+
+  /**
+   * 编辑
+   * @param record
+   */
+  function edit(record) {
+    title.value = disableSubmit.value ? '详情' : '编辑';
+    visible.value = true;
+    nextTick(() => {
+      registerForm.value.edit(record);
+    });
+  }
+
+  /**
+   * 确定按钮点击事件
+   */
+  function handleOk() {
+    registerForm.value.submitForm();
+  }
+
+  /**
+   * form保存回调事件
+   */
+  function submitCallback() {
+    handleCancel();
+    emit('success');
+  }
+
+  /**
+   * 取消按钮回调事件
+   */
+  function handleCancel() {
+    visible.value = false;
+  }
+
+  defineExpose({
+    add,
+    edit,
+    disableSubmit,
+  });
+</script>
+
+<style lang="less">
+  /**隐藏样式-modal确定按钮 */
+  .jee-hidden {
+    display: none !important;
+  }
+</style>
+<style lang="less" scoped></style>

Some files were not shown because too many files changed in this diff