Browse Source

feat: 更新@jy/pc-ui包

cuiyalong 1 year ago
parent
commit
ccd98097ff

+ 28 - 0
packages/pc-ui/.gitignore

@@ -0,0 +1,28 @@
+.DS_Store
+/node_modules
+/dist
+/page_big_pc
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# compressed files
+*.rar
+*.zip
+*.7z
+*.tar

+ 3 - 0
packages/pc-ui/.npmrc

@@ -0,0 +1,3 @@
+registry=http://192.168.3.71:4873/
+@jianyu:registry=http://192.168.3.71:4873/
+element-ui:registry=http://192.168.3.71:4873/

+ 85 - 0
packages/pc-ui/README.md

@@ -0,0 +1,85 @@
+## @jy/pc-ui
+
+> 剑鱼pc端ui组件库
+
+### 开发库
+
+```
+yarn
+npm run dev
+
+运行之后在App.vue中引入你需要调试的组件
+```
+
+### 调试
+
+[yarn link调试本地包](https://juejin.cn/post/6844904164468768776)
+[npm link调试本地包](https://juejin.cn/post/7252911555524067386)
+
+1. 找到本地**包目录**(yarn link与npm link使用方式相同)
+
+   ```bash
+   cd ./packages/pc-ui
+   ```
+
+2. 使用`npm link`将本地包注册到全局
+
+   ```bash
+   cd ./packages/pc-ui
+   yarn link
+   ```
+
+3. 找到**项目目录**,安装本地调试包
+
+   ```bash
+   cd ./page_bigmember_pc
+   yarn link @jy/pc-ui
+   ```
+
+4. 调试完毕,在**项目目录**解除链接
+
+   ```bash
+   yarn unlink @jy/pc-ui
+   ```
+
+5. 解除link
+
+   ```bash
+   $ 解除项目与模块的link,在项目目录下,yarn unlink 模块名
+   $ 解除模块全局的link,在模块目录下,yarn unlink 模块名
+   ```
+
+
+
+### 引用库
+
+#### 安装
+
+```json
+1. 在package.json的dependencies中添加
+{
+  "dependencies": {
+    "@jy/pc": "^0.0.3",
+  }
+}
+2. 执行命令 yarn
+```
+
+#### 使用(注意!!!)
+
+```js
+// Toast组件需要在main.js中注册
+// 如果不初始化,则可能会在部分组件使用this.$toast时候报错
+import Vue from 'vue'
+import { Toast } from '@jy/pc-ui'
+Vue.use(Toast)
+```
+
+
+#### 调用示例
+
+```js
+// 库一共提供了以下内容
+import { KeywordsGroup } from '@jy/pc-ui'
+```
+

+ 20 - 0
packages/pc-ui/example/index.html

@@ -0,0 +1,20 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <link rel="icon" href="/favicon.ico" />
+    <title>Example</title>
+  </head>
+  <body>
+    <noscript>
+      <strong
+        >We're sorry but this page doesn't work properly without JavaScript
+        enabled. Please enable it to continue.</strong
+      >
+    </noscript>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 20 - 0
packages/pc-ui/index.html

@@ -0,0 +1,20 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <link rel="icon" href="/favicon.ico" />
+    <title>Example</title>
+  </head>
+  <body>
+    <noscript>
+      <strong
+        >We're sorry but this page doesn't work properly without JavaScript
+        enabled. Please enable it to continue.</strong
+      >
+    </noscript>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 62 - 0
packages/pc-ui/package.json

@@ -0,0 +1,62 @@
+{
+  "name": "@jy/pc-ui",
+  "description": "剑鱼前端pc端组件库",
+  "author": "cuiyalong",
+  "version": "0.0.3",
+  "keywords": [
+    "jianyu",
+    "ui",
+    "pc"
+  ],
+  "main": "src/index.js",
+  "files": [
+    "src"
+  ],
+  "private": false,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "lint": "eslint \"**/*.{vue,ts,js}\"",
+    "lint:fix": "eslint \"**/*.{vue,ts,js}\" --fix",
+    "test": "vitest",
+    "format": "prettier --write --cache ."
+  },
+  "dependencies": {
+    "core-js": "^3.29.1",
+    "dayjs": "^1.11.7",
+    "element-ui": "^2.15.16-rc",
+    "lodash": "^4.17.21",
+    "qs": "^6.11.2",
+    "vue": "^2.7.16"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-legacy": "^4.0.4",
+    "@vitejs/plugin-vue2": "^2.2.0",
+    "autoprefixer": "^10.4.14",
+    "mockjs": "^1.1.0",
+    "postcss-prefix-selector": "^1.16.0",
+    "sass": "^1.63.2",
+    "vite": "^4.2.1",
+    "vitest": "^0.34.6",
+    "vue-template-compiler": "^2.7.14",
+    "vue-template-es2015-compiler": "^1.9.1"
+  },
+  "engines": {
+    "node": ">=12.0.0"
+  },
+  "postcss": {
+    "plugins": {
+      "autoprefixer": {}
+    }
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ],
+  "homepage": "https://jygit.jydev.jianyu360.cn/jianyu/web/packages/pc-ui",
+  "repository": {
+    "type": "git",
+    "url": "https://jygit.jydev.jianyu360.cn/jianyu/web/packages/pc-ui"
+  }
+}

+ 22 - 0
packages/pc-ui/src/App.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="page">
+    <KeyConfig></KeyConfig>
+  </div>
+</template>
+
+<script>
+import KeyConfig from './packages/keywords/index.vue';
+export default {
+  components: {
+    KeyConfig
+  },
+  data() {
+    return {}
+  },
+  created() {},
+  methods: {}
+}
+</script>
+<style lang="scss">
+
+</style>

BIN
packages/pc-ui/src/assets/images/icon/help.png


BIN
packages/pc-ui/src/assets/images/icon/icon-checked.png


BIN
packages/pc-ui/src/assets/images/icon/icon-delete.png


BIN
packages/pc-ui/src/assets/images/icon/icon-edit.png


BIN
packages/pc-ui/src/assets/images/setkey-dialog.png


+ 31 - 0
packages/pc-ui/src/assets/style/_mixin.scss

@@ -0,0 +1,31 @@
+// 公用函数
+@function addPx($a, $b) {
+  @return $a + $b;
+}
+
+@function addTop($a) {
+  @return $topNavHeight + $a;
+}
+
+@function addFooter($a) {
+  @return $footerHeight + $a;
+}
+
+@mixin diy-icon($name, $width: 24, $height: 24) {
+  ::v-deep .el-icon-jy-#{$name} {
+    background: url('~@/assets/images/icon/#{$name}.png') no-repeat;
+    background-size: cover;
+    display: inline-block;
+    width: #{$width}px;
+    height: #{$height}px;
+  }
+}
+
+@mixin ellipsis($lines: 1) {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: #{$lines};
+  -webkit-box-orient: vertical;
+  text-align: justify;
+}

+ 35 - 0
packages/pc-ui/src/assets/style/_variables.scss

@@ -0,0 +1,35 @@
+// 导航栏
+$topNavHeight: 77px;
+
+// 底部栏
+$footerHeight: 364px;
+
+$color_main: #2cb7ca;
+
+// Background
+// 透明背景色使用时,需要配合白色背景使用
+$color_main_background: rgb($color_main,.1);
+
+$bg-retrieve: #010c28;
+$bg-button--default: linear-gradient(84deg, #af9552 0%, #efda98 100%);
+$bg-card--default: linear-gradient(#031242 0%, #010e36 100%);
+$bg-button--tran: linear-gradient(#53f1dd 0%, #07907e 100%);
+$bg-color-1: linear-gradient(90deg, #9f1b89 0%, #bb36a5 100%);
+$bg-color-2: linear-gradient(90deg, #46b5d1 0%, #8cd5e4 100%);
+$bg-color-3: linear-gradient(90deg, #f5ab48 0%, #f4ce8f 100%);
+$bg-color-4: linear-gradient(90deg, #f83f4f 0%, #f38797 100%);
+$bg-color-5: linear-gradient(90deg, #41af92 0%, #84ceb7 100%);
+$bg-color-6: linear-gradient(90deg, #7446a0 0%, #a380c4 100%);
+
+$bg-less: #f5feff;
+
+$font-text--title: 17px;
+
+$color-text--default: #1d1d1d;
+$color-text--active: $color_main;
+$color-text--highlight: $color_main;
+$color-text--less: #2abed1;
+
+$color-input--default: #1d1d1d;
+
+$bg-main-color: #fff;

+ 172 - 0
packages/pc-ui/src/assets/style/common.scss

@@ -0,0 +1,172 @@
+@import './variables';
+@import './mixin';
+
+html {
+  height: 100%;
+  // overflow-y: auto;
+}
+
+body {
+  background-color: #f2f2f4;
+}
+
+.highlight-text {
+  color: $color-text--highlight;
+}
+
+.pointer {
+  cursor: pointer;
+}
+
+[class*='no-select'] {
+  user-select: none;
+}
+
+.ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-align: justify;
+}
+
+@for $i from 2 through 5 {
+  .ellipsis-#{$i} {
+    @include ellipsis($i);
+  }
+}
+
+/* 超过2行省略号显示 */
+.ellipsis-2 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  text-align: justify;
+}
+
+/* 超过3行省略号显示 */
+.ellipsis-3 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  -webkit-box-orient: vertical;
+}
+
+::-webkit-scrollbar {
+  /*滚动条整体样式*/
+  width: 8px;
+}
+::-webkit-scrollbar-thumb {
+  /*滚动条里面小方块*/
+  border-radius: 3px;
+  background-color: #ececec;
+  opacity: 0.15;
+}
+.scrollbar {
+  &::-webkit-scrollbar {
+    /*滚动条整体样式*/
+    width: 8px;
+  }
+  &::-webkit-scrollbar-thumb {
+    /*滚动条里面小方块*/
+    border-radius: 3px;
+    background-color: #ececec;
+    opacity: 0.15;
+  }
+}
+
+*:focus-visible {
+  outline: none;
+}
+
+// 清除 input type=number 默认样式
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+  -webkit-appearance: none;
+}
+input[type='number'] {
+  -moz-appearance: textfield;
+}
+
+.flex-w-100 {
+  width: 100%;
+  flex: 1;
+}
+.flex-r-c {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  &.center {
+    align-items: center;
+    justify-content: center;
+    &.sb {
+      justify-content: space-between;
+    }
+  }
+  &.left {
+    justify-content: flex-start;
+  }
+  &.right {
+    justify-content: flex-start;
+  }
+  .bottom {
+    align-items: flex-end;
+  }
+  &.wrap {
+    flex-wrap: wrap;
+  }
+}
+.flex-c-c {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  &.center {
+    align-items: center;
+    justify-content: center;
+  }
+  &.right {
+    align-items: flex-end;
+  }
+  &.left {
+    align-items: flex-start;
+  }
+}
+.f-share {
+  padding: 6px 4px !important;
+  box-shadow: 0px 0px 28px 0px #999;
+  border: none !important;
+  .popper__arrow {
+    border: none !important;
+  }
+}
+
+.show-underline {
+  .keyword.hide-underline {
+    border-bottom-width: 1px;
+  }
+}
+.keyword-underline {
+  border-bottom: 1px solid $color-main;
+  padding-bottom: 1px;
+  &.project-name,
+  &.winner-name {
+    cursor: pointer;
+  }
+}
+.keyword.hide-underline {
+  border-width: 0;
+}
+
+.iconfont {
+  &.icon-jiankong {
+    color: #9b9ca3;
+  }
+  &.icon-yijiankong {
+    color: #ff9f40;
+  }
+}
+
+
+

+ 289 - 0
packages/pc-ui/src/assets/style/reset-ele.scss

@@ -0,0 +1,289 @@
+@import './_mixin';
+@import './_variables';
+
+// 分页样式重置
+.el-pagination-container {
+  position: relative;
+  margin-top: 32px;
+  margin-right: 16px;
+  padding-bottom: 60px;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  .el-pagination {
+    position: absolute;
+    right: 0;
+  }
+}
+.el-pagination.is-background .el-pager {
+  li {
+    background-color: #fff;
+    border: 1px solid rgba($color: #000, $alpha: 0.05);
+  }
+
+  li:not(.disabled).active,
+  li:not(.disabled):hover {
+    color: #fff;
+    background-color: $color-text--highlight;
+  }
+}
+
+// 修改输入框默认focus边框颜色
+.el-input.is-active .el-input__inner,
+.el-input__inner:focus {
+  border-color: $color-text--highlight;
+}
+
+.el-checkbox__inner {
+  width: 16px;
+  height: 16px;
+  border-radius: 3px;
+  &::after {
+    border-width: 2px;
+    left: 5px;
+    top: 1px;
+  }
+}
+
+.el-button--main,
+.el-button--confirm {
+  border-color: $color-text--highlight;
+  background: $color-text--highlight;
+  border-radius: 6px;
+  padding: 8px 16px;
+  box-sizing: border-box;
+  font-size: 14px;
+  font-weight: 400;
+  color: #fff;
+  line-height: 24px;
+  &:hover,
+  &:focus {
+    border-color: $color-text--highlight;
+    background: $color-text--highlight;
+    color: #fff;
+  }
+}
+
+.el-button--less,
+.el-button--cancel {
+  border-color: $color-text--less;
+  background: $bg-less;
+  border-radius: 6px;
+  padding: 6px 16px;
+  box-sizing: border-box;
+  font-size: 16px;
+  line-height: 22px;
+  font-weight: 400;
+  color: $color-text--less;
+  &:hover,
+  &:focus {
+    border-color: $color-text--highlight;
+    background: $color-text--highlight;
+    color: #fff;
+  }
+}
+
+.el-link {
+  &.el-link--default {
+    &:hover {
+      color: $color-text--highlight;
+    }
+  }
+}
+
+.el-loading-mask {
+  transition: opacity 1s;
+}
+
+
+.custom-message-box {
+  width: 380px !important;
+  border-radius: 8px;
+  .custom-confirm-btn {
+    margin-top: 12px;
+    width: 132px;
+    height: 36px;
+    background: #2cb7ca;
+    border-radius: 6px;
+    border: 0;
+    font-size: 16px;
+    &:hover {
+      background: #2cb7ca;
+    }
+  }
+
+  .custom-cancel-btn,
+  .custom-cancel-btn:hover,
+  .custom-cancel-btn:focus{
+    width: 132px;
+    height: 34px;
+    background-color: #fff;
+    border: 1px solid #DCDFE6;
+    color: #686868;
+    font-size: 16px;
+  }
+
+  .el-message-box__message,
+  .message-text {
+    font-size: 14px;
+    color: #686868;
+    line-height: 24px;
+  }
+}
+.el-popper {
+  li {
+    float: none;
+  }
+}
+
+.el-pagination__jump {
+  color: #686868 !important;
+}
+.el-pagination.is-background .el-pagination__confirm {
+  width: 52px;
+  text-align: center;
+  color: #1d1d1d;
+}
+
+// 分页组件页码选择select下拉框样式
+.pagination-custom-select {
+  top: -138px !important;
+  left: 4px !important;
+  min-width: 100px !important;
+  margin-top: 0px !important;
+  border-radius: 2px !important;
+  transform-origin: center bottom !important;
+  .el-scrollbar {
+    height: 136px;
+    border-radius: 2px;
+  }
+  .el-scrollbar__wrap {
+    height: 136px !important;
+    overflow: unset !important;
+  }
+  .el-select-dropdown__list {
+    padding: 0;
+    max-width: 100px !important;
+  }
+  .el-select-dropdown__item {
+    width: 100%;
+    text-align: center;
+    color: #1d1d1d;
+    border-bottom: 1px solid #ececec;
+    text-overflow: unset !important;
+  }
+  .el-select-dropdown__item.selected {
+    color: #2cb7ca;
+  }
+  .el-select-dropdown__item.hover,
+  .el-select-dropdown__item:hover {
+    background: #2cb7ca;
+    color: #fff;
+  }
+  .el-scrollbar__bar.is-horizontal {
+    height: 0;
+  }
+  .popper__arrow {
+    display: none !important;
+  }
+  .el-select-dropdown__item.selected {
+    color: #2cb7ca;
+  }
+  .el-select-dropdown__item.hover,
+  .el-select-dropdown__item:hover {
+    background: #2cb7ca;
+    color: #fff;
+  }
+  .el-select-dropdown__wrap {
+    margin-bottom: -18px !important;
+  }
+}
+
+.el-popover.reset-el-popover {
+  padding: 0;
+  border: none;
+  box-shadow: none;
+  &.no-content {
+    display: none;
+  }
+}
+
+.el-radio.jy-radio {
+  margin-right: 16px;
+  .el-radio__input.is-checked .el-radio__inner {
+    background-color: transparent;
+  }
+  .el-radio__inner {
+    width: 18px;
+    height: 18px;
+  }
+  .el-radio__inner::after {
+    width: 9px;
+    height: 9px;
+    background-color: #2abed1;
+  }
+}
+
+// 从selector-cascader-common.scss中提取成全局
+.selector-cascader {
+  $min-width: 204px;
+
+  position: relative;
+  > .el-popper[x-placement^='bottom'] {
+    margin-top: 0;
+  }
+  > .el-popover,
+  > .el-select-dropdown {
+    padding: 0;
+    left: 0 !important;
+    border-color: $color_main;
+
+    .el-cascader-menu__wrap {
+      height: 204px;
+    }
+    .el-cascader-menu {
+      min-width: 160px;
+      color: #1d1d1d;
+    }
+    .el-cascader-node,
+    .el-select-dropdown__item {
+      height: 30px;
+      line-height: 30px;
+    }
+  }
+
+  .popper__arrow {
+    display: none !important;
+  }
+  .el-cascader-panel.is-bordered {
+    border: none;
+  }
+
+  // 此处公共样式不定义。可在组件内自行修改
+  // .el-cascader-menu__list {
+  //   min-width: $min-width - 2px;
+  // }
+
+  .virtual-cascader,
+  .el-cascader {
+    min-width: $min-width;
+    height: 30px;
+    line-height: 30px;
+  }
+  .virtual-input,
+  .el-input {
+    height: 100%;
+    .el-input__inner {
+      height: 30px;
+      line-height: 30px;
+      font-size: 14px;
+      color: #1d1d1d;
+      border-color: #e0e0e0;
+    }
+    .el-input__icon {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}

+ 12 - 0
packages/pc-ui/src/index.js

@@ -0,0 +1,12 @@
+import './assets/style/common.scss'
+import './assets/style/reset-ele.scss'
+
+import KeywordsGroup from './packages/keywords/index.vue'
+import CommonDialog from './packages/dialog/common-dialog.vue'
+import Toast from './packages/toast/index'
+
+export {
+  Toast,
+  KeywordsGroup,
+  CommonDialog
+}

+ 26 - 0
packages/pc-ui/src/main.js

@@ -0,0 +1,26 @@
+import Vue from 'vue'
+import App from './App.vue'
+import ElementUI from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css'
+import { Loading, Message, MessageBox } from 'element-ui'
+import Toast from './packages/toast/index'
+
+import './assets/style/common.scss'
+import './assets/style/reset-ele.scss'
+
+Vue.use(Loading.directive)
+Vue.use(ElementUI)
+// 如果不初始化,则可能会在部分组件使用this.$toast时候报错
+Vue.use(Toast)
+
+// # 修复v-charts 不适配 vue2.7x https://github.com/ElemeFE/v-charts/issues/934
+Vue._watchers = Vue.prototype._watchers = []
+
+Vue.prototype.$message = Message
+Vue.prototype.$alert = MessageBox.alert
+Vue.prototype.$confirm = MessageBox.confirm
+Vue.config.productionTip = false
+
+const app = new Vue({
+  render: h => h(App)
+}).$mount('#app')

+ 140 - 0
packages/pc-ui/src/packages/dialog/common-dialog.vue

@@ -0,0 +1,140 @@
+<template>
+  <el-dialog
+    class="custom-dialog"
+    :custom-class="customClass"
+    v-bind="$props"
+    :show-close="showClose"
+    :visible="visible"
+    @update:visible="update"
+    @open="$emit('open')"
+    @opened="$emit('opened')"
+    @close="$emit('close')"
+    @closed="$emit('closed')"
+  >
+    <slot name="default"></slot>
+    <span slot="footer" v-if="showFooter" class="dialog-footer">
+      <slot name="footer">
+        <button class="action-button cancel" @click="onClickCancel">
+          取消
+        </button>
+        <button
+          class="action-button confirm"
+          :disabled="disabled"
+          @click="onClickConfirm"
+        >
+          确定
+        </button>
+      </slot>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import { Dialog, Button } from 'element-ui'
+
+export default {
+  name: 'CustomDialog',
+  components: {
+    [Dialog.name]: Dialog,
+    [Button.name]: Button
+  },
+  props: {
+    visible: Boolean,
+    showClose: Boolean,
+    comMount: {
+      type: String,
+      default: ''
+    },
+    showFooter: {
+      type: Boolean,
+      default() {
+        return true
+      }
+    },
+    top: String,
+    title: {
+      type: String,
+      default: ''
+    },
+    width: {
+      type: String,
+      default: '30%'
+    },
+    'show-close': {
+      type: Boolean,
+      default: false
+    },
+    center: {
+      type: Boolean,
+      default: true
+    },
+    customClass: {
+      type: String,
+      default: ''
+    },
+    disabled: Boolean
+  },
+  methods: {
+    update(e) {
+      this.$emit('update:visible', e)
+    },
+    onClickCancel() {
+      this.$emit('cancel')
+    },
+    onClickConfirm() {
+      this.$emit('confirm')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+::v-deep {
+  .el-dialog {
+    border-radius: 8px;
+    background-color: transparent;
+    box-shadow: none;
+  }
+  .el-dialog__body {
+    color: #686868;
+    font-size: 14px;
+    line-height: 22px;
+  }
+  .dialog-footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .action-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex: 1;
+    height: 36px;
+    border-radius: 6px;
+    &.cancel {
+      border: 1px solid #e0e0e0;
+      background-color: #fff;
+      color: #686868;
+    }
+    &.confirm {
+      border: 1px solid #2cb7ca;
+      background-color: #2cb7ca;
+      color: #fff;
+      &:disabled {
+        opacity: 0.5;
+      }
+    }
+    &:not(:last-of-type) {
+      margin-right: 48px;
+    }
+  }
+}
+.text-center {
+  ::v-deep {
+    .el-dialog__body {
+      text-align: center;
+    }
+  }
+}
+</style>

+ 525 - 0
packages/pc-ui/src/packages/keywords/index.vue

@@ -0,0 +1,525 @@
+<template>
+  <div class="keywords-config">
+    <div class="key-title">
+      <div>
+        <span>关键词组设置</span>
+        <span style="font-size: 14px"
+          ><em style="color: #2cb7ca"> {{ keyCounts }}</em
+          >/{{ maxCount }}</span
+        >
+        <p
+          style="
+            display: flex;
+            align-items: center;
+            font-size: 14px;
+            color: #2cb7ca;
+          "
+        >
+          注:任意1组关键词组匹配成功即推送相关信息
+          <img
+            @click="showMatchTipDialog"
+            src="../../assets/images/icon/help.png"
+            class="icon-help-img"
+          />
+        </p>
+      </div>
+      <div class="add-classify" @click="addClassifyFn">
+        <i class="el-icon-plus"></i> 新增分类
+      </div>
+    </div>
+    <div class="key-content">
+      <KeyList
+        ref="keyConfigRef"
+        :getPushCount="getPushCount"
+        :getRecommend="getRecommend"
+        :list="keywordsList"
+        :max-count="maxCount"
+        :key="sort"
+        @update="onUpdateKey"
+      ></KeyList>
+    </div>
+    <el-dialog
+      custom-class="sub-dialog small-dialog"
+      :visible.sync="add.dialog"
+      :close-on-click-modal="false"
+      :show-close="false"
+      center
+      width="460px"
+    >
+      <KeyCard @onCancel="add.dialog = false" @onConfirm="confirmEditClassFn">
+        <div slot="header">新增关键词分类</div>
+        <div class="class-edit-content">
+          <div class="item">
+            <div class="item-label">关键词分类:</div>
+            <div class="item-value">
+              <el-input
+                class="custom-long-input"
+                v-model.trim="add.className"
+                maxlength="20"
+                placeholder="请输入关键词分类"
+              ></el-input>
+            </div>
+          </div>
+        </div>
+      </KeyCard>
+    </el-dialog>
+    <el-dialog
+      custom-class="sub-dialog small-dialog"
+      :visible.sync="add.dialog"
+      :close-on-click-modal="false"
+      :show-close="false"
+      center
+      width="460px"
+    >
+      <KeyCard @onCancel="add.dialog = false" @onConfirm="confirmEditClassFn">
+        <div slot="header">新增关键词分类</div>
+        <div class="class-edit-content">
+          <div class="item">
+            <div class="item-label">关键词分类:</div>
+            <div class="item-value">
+              <el-input
+                class="custom-long-input"
+                v-model.trim="add.className"
+                maxlength="20"
+                placeholder="请输入关键词分类"
+              ></el-input>
+            </div>
+          </div>
+        </div>
+      </KeyCard>
+    </el-dialog>
+    <commonDialog
+      width="500px"
+      class="setkey-dialog"
+      :visible="showSetKeyDialog"
+      :show-footer="false"
+    >
+      <img
+        @click="showSetKeyDialog = false"
+        class="setkey-img"
+        src="../../assets/images/setkey-dialog.png"
+        alt=""
+      />
+    </commonDialog>
+  </div>
+</template>
+<script>
+import { Input, Button, Dialog, RadioGroup, Radio } from 'element-ui'
+import commonDialog from '../dialog/common-dialog.vue'
+import KeyCard from './selector-card.vue'
+import KeyList from './keywords-list.vue'
+export default {
+  name: 'keyConfig',
+  components: {
+    [Input.name]: Input,
+    [Dialog.name]: Dialog,
+    [Button.name]: Button,
+    [RadioGroup.name]: RadioGroup,
+    [Radio.name]: Radio,
+    commonDialog,
+    KeyCard,
+    KeyList
+  },
+  props: {
+    getPushCount: Function,
+    getRecommend: Function,
+    list: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    maxCount: {
+      type: Number,
+      default: 300
+    }
+  },
+  data() {
+    return {
+      keywordsList: this.list,
+      add: {
+        dialog: false,
+        className: ''
+      },
+      sort: 1,
+      showSetKeyDialog: false
+    }
+  },
+  computed: {
+    keyCounts() {
+      let count = 0
+      this.keywordsList.forEach((v) => {
+        if (v && v.a_key) {
+          count += v.a_key.length
+        }
+      })
+      return count
+    }
+  },
+  methods: {
+    showMatchTipDialog() {
+      this.showSetKeyDialog = true
+    },
+    onUpdateKey(data) {
+      this.keywordsList = data || []
+      this.$emit('update', this.keywordsList)
+    },
+    parentGetCurEdit() {
+      const t = this.$refs.keyConfigRef.getCurEdit()
+      return t
+    },
+    // 新增关键词分类弹框
+    addClassifyFn() {
+      const t = this.parentGetCurEdit()
+      if (t) return this.$toast('请先保存或取消正在操作的关键词组')
+      this.add.className = ''
+      this.add.dialog = true
+    },
+    // 确认添加分类
+    confirmEditClassFn() {
+      const classArr = this.getClassArray()
+      const list = this.keywordsList
+      if (classArr.indexOf(this.add.className) > -1) {
+        return this.$message({
+          type: 'warning',
+          message: '分类名不能重复'
+        })
+      } else if (this.add.className === '') {
+        return this.$message({
+          type: 'warning',
+          message: '分类名不能为空'
+        })
+      }
+      if (list.length === 1 && list[0].s_item === '未分类') {
+        list[0].s_item = this.add.className
+        list[0].updatetime = parseInt(Date.now() / 1000)
+      } else {
+        list.push({
+          s_item: this.add.className,
+          a_key: [],
+          showForm: false,
+          updatetime: parseInt(Date.now() / 1000)
+        })
+      }
+      this.add.dialog = false
+      this.sort = Date.now()
+      this.$refs.keyConfigRef.$forceUpdate()
+      this.$emit('update', this.keywordsList)
+    },
+    // 获取所有分类名
+    getClassArray() {
+      const data = this.keywordsList
+      const classArr = []
+      data.forEach((v) => {
+        if (v.s_item) {
+          classArr.push(v.s_item)
+        }
+      })
+      return classArr
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .popupBox_ {
+    left: unset;
+    right: 70px;
+  }
+  .sub-dialog .el-dialog__header,
+  .sub-dialog .el-dialog__body {
+    padding: 0;
+  }
+  .sub-dialog {
+    .el-dialog__header,
+    .sub-dialog .el-dialog__body {
+      padding: 0;
+    }
+    &.small-dialog {
+      .selector-card,
+      .selector-card.s-card {
+        height: auto;
+      }
+    }
+    .class-edit-content {
+      padding: 30px;
+      width: 100%;
+      height: auto;
+      max-height: 340px;
+      margin: 0 auto;
+      border: 1px solid #ececec;
+      overflow-y: scroll;
+      box-sizing: border-box;
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin-bottom: 10px;
+      }
+      .item-label {
+        margin-right: 8px;
+        // min-width: 120px;
+        height: 40px;
+        color: #1d1d1d;
+        font-size: 14px;
+        line-height: 40px;
+        text-align: right;
+      }
+      .item-label-required:before {
+        content: '*';
+        color: #f56c6c;
+        margin-right: 2px;
+      }
+      .item-value {
+        flex: 1;
+      }
+    }
+    .delete-class {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      padding: 32px 32px 42px;
+      background: #ffffff;
+      border-radius: 6px;
+      .delete-class-header {
+        font-size: 18px;
+        line-height: 28px;
+        color: #1d1d1d;
+      }
+      .delete-class-content {
+        padding: 20px 0 42px;
+        color: #686868;
+        font-size: 14px;
+        line-height: 22px;
+      }
+      .delete-class-footer {
+        display: flex;
+        align-items: center;
+        .confirm,
+        .cancel {
+          padding: 10px 50px;
+          margin: 0 20px;
+        }
+        .el-button--primary {
+          color: #2cb7ca;
+          background: none;
+          border-color: #2cb7ca;
+          &:hover {
+            color: #fff;
+            background-color: #2cb7ca;
+          }
+        }
+        .el-button--default {
+          &:hover,
+          &:active {
+            color: #2cb7ca;
+            border-color: #2cb7ca;
+            background: #eaf8fa;
+          }
+        }
+      }
+    }
+    // 修改输入框默认focus边框颜色
+    .el-input.is-active .el-input__inner,
+    .el-input__inner:focus {
+      border-color: #2cb7ca;
+    }
+  }
+  .small-dialog {
+    border-radius: 8px;
+    .dialog-text {
+      margin-top: 4px;
+      font-size: 14px;
+      color: #686868;
+      line-height: 22px;
+      text-align: center;
+    }
+    .know-btn {
+      background: #2cb7ca;
+      border-radius: 6px;
+      border: 0;
+    }
+    .dialog-update-black {
+      color: #1d1d1d;
+      line-height: 22px;
+      text-align: justify;
+    }
+    .dialog-update-gray {
+      margin-top: 20px;
+      color: #686868;
+      line-height: 22px;
+      text-align: justify;
+    }
+  }
+  .email-dialog {
+    padding: 32px;
+    border-radius: 8px;
+    .el-dialog__body {
+      padding: 20px 0;
+      .error-tips {
+        margin-top: 4px;
+        font-size: 14px;
+        line-height: 22px;
+        color: #ff3a20;
+      }
+    }
+    .el-input__inner {
+      height: 36px;
+      line-height: 36px;
+    }
+    .el-dialog__footer {
+      padding: 0;
+      .dialog-footer {
+        width: 100%;
+        display: flex;
+        justify-content: space-between;
+      }
+      .el-button {
+        width: 132px;
+        height: 36px;
+        font-size: 16px;
+        line-height: 36px;
+        padding: 0;
+      }
+      .el-button--primary {
+        border-color: #2cb7ca;
+        background: #2cb7ca;
+        &:disabled {
+          opacity: 0.6;
+        }
+      }
+      .el-button--default {
+        background: #fff;
+        &:hover {
+          color: #686868;
+          border-color: #e0e0e0;
+        }
+      }
+    }
+  }
+  .tip-dialog {
+    left: unset;
+    right: 70px;
+    .el-button--primary,
+    .el-button--primary:hover,
+    .el-button--primary:focus {
+      width: 132px;
+      height: 36px;
+      margin-right: 52px;
+      text-align: center;
+      background: #2cb7ca;
+      border-radius: 6px;
+      font-style: 16px;
+      color: #fff;
+      border: 0;
+    }
+    .el-dialog {
+      border-radius: 8px;
+    }
+    .el-dialog__header {
+      padding: 32px 0 0;
+    }
+    .el-dialog__body {
+      padding: 20px 32px 32px;
+      color: #686868;
+      font-size: 14px;
+      line-height: 22px;
+      text-align: center;
+    }
+    .el-dialog__body i {
+      color: #2cb7ca;
+    }
+    .el-button {
+      width: 132px;
+      height: 36px;
+      padding: 0;
+      text-align: center;
+      font-size: 16px;
+    }
+    .el-dialog__footer {
+      padding: 0 0 32px;
+    }
+  }
+  .custom-dialog.setkey-dialog {
+    z-index: 2100 !important;
+    .el-dialog__header {
+      display: none;
+    }
+    .el-dialog__body {
+      padding: 0;
+    }
+  }
+}
+
+
+
+.keywords-config {
+  background: #fff;
+  .key-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 13px 0 8px;
+    font-size: 18px;
+    color: #1d1d1d;
+    line-height: 28px;
+    border-bottom: 1px solid #ececec;
+    .icon-help-img {
+      display: inline-block;
+      width: 18px;
+      height: 18px;
+      margin: 0 7px 0 8px;
+      cursor: pointer;
+    }
+    .add-classify {
+      width: 110px;
+      height: 38px;
+      line-height: 38px;
+      border: 1px solid #2cb7ca;
+      border-radius: 6px;
+      text-align: center;
+      color: #2cb7ca;
+      font-size: 14px;
+      cursor: pointer;
+    }
+  }
+  .key-content {
+    padding: 20px 0;
+    .classify-title {
+      display: flex;
+      align-items: center;
+      .title-text {
+        font-size: 16px;
+        color: #1d1d1d;
+      }
+      .icon-edit,
+      .icon-delete {
+        display: inline-block;
+        width: 16px;
+        height: 16px;
+        background-repeat: no-repeat;
+        background-position: center center;
+        cursor: pointer;
+        &::before {
+          content: '' !important;
+        }
+      }
+      .icon-edit {
+        margin: 0 10px;
+        background-image: url('../../assets/images/icon/icon-edit.png');
+        background-size: contain;
+      }
+      .icon-delete {
+        background-image: url('../../assets/images/icon/icon-delete.png');
+        background-size: contain;
+      }
+    }
+  }
+}
+
+.setkey-img {
+  width: 500px;
+  cursor: pointer;
+}
+</style>

+ 687 - 0
packages/pc-ui/src/packages/keywords/keywords-edit.vue

@@ -0,0 +1,687 @@
+<template>
+  <div class="edit-form">
+    <div class="input-box">
+      <div class="input-box-title">{{ title }}关键词组</div>
+      <div class="item">
+        <div class="item-label">关键词:</div>
+        <div class="item-value">
+          <el-input
+            type="textarea"
+            autosize
+            resize="none"
+            placeholder="请输入关键词,多个关键词用空格隔开,例如:税务局 软件"
+            debounce="600"
+            maxlength="200"
+            @input="keywordsInput"
+            @blur="keywordsBlur"
+            v-model="cur.key"
+          >
+          </el-input>
+        </div>
+      </div>
+      <div class="item" v-if="sameWordsList && sameWordsList.length > 0">
+        <div class="item-label"></div>
+        <div class="item-value">
+          <div class="recommend">
+            <div class="recommend-title">
+              <span>相似订阅推荐</span>
+              <span
+                class="batch-btn"
+                v-if="sameWordsList && sameWordsList.length > 9"
+                @click="getRecommendFn"
+                ><i class="el-icon-refresh"></i> 换一批</span
+              >
+            </div>
+            <div class="recommend-main">
+              <div
+                class="r-list"
+                :class="{ active: s.selected }"
+                v-for="(s, j) in sameWordsList.slice(0, 9)"
+                :key="'00' + j"
+                @click="addSameItem($event, s)"
+              >
+                {{ s.word }}
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="item no-center">
+        <div class="item-label">匹配模式:</div>
+        <div class="item-value">
+          <el-radio-group
+            v-model="cur.matchway"
+            @change="chooseMatchWay($event)"
+          >
+            <el-radio class="radio-item" name="matchway" :label="1"
+              >模糊<span class="math-tips"
+                >(任意1个关键词匹配成功即推送)</span
+              ></el-radio
+            >
+            <el-radio name="matchway" :label="0"
+              >精准<span class="math-tips"
+                >(同时包含所有关键词才推送)</span
+              ></el-radio
+            >
+          </el-radio-group>
+        </div>
+      </div>
+      <div class="item" v-if="showNotWay" key="notway">
+        <div class="item-label">排除词:</div>
+        <div class="item-value">
+          <el-input
+            type="textarea"
+            autosize
+            resize="none"
+            placeholder="请输入排除词,多个排除词用空格隔开,不希望接收,与关键词互斥"
+            @blur="notKeyBlur"
+            v-model="cur.notkey"
+          >
+          </el-input>
+        </div>
+      </div>
+      <div class="item" v-else key="notway2">
+        <div class="item-label"></div>
+        <div class="item-value">
+          <div class="add-words-btn" @click="showNotWay = true">
+            +添加排除词(不希望接收,与关键词互斥)
+          </div>
+        </div>
+      </div>
+      <div class="little-tips" v-show="littleTip">
+        当前匹配信息过少,请适当修改关键词
+      </div>
+      <div class="btn-groups">
+        <button
+          type="button"
+          :disabled="keyDisabled"
+          class="confirm-btn"
+          @click="submitKeywords"
+        >
+          保存<span style="font-size: 12px" v-if="getPushCount">
+            (近3个月内共匹配{{ pushCount }}条信息)</span
+          >
+        </button>
+        <button type="button" class="cancle-btn" @click="cancelEdit">
+          取消
+        </button>
+      </div>
+    </div>
+    <!-- 关键词重复提示 -->
+    <el-dialog
+      custom-class="small-dialog"
+      title="新增关键词组"
+      :visible.sync="dialog.repeat"
+      width="380px"
+      center
+      :show-close="false"
+    >
+      <p class="dialog-text">该组关键词已存在,请勿重复添加</p>
+      <span slot="footer" class="dialog-footer">
+        <el-button
+          class="know-btn"
+          type="primary"
+          @click="dialog.repeat = false"
+          >我知道了</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import { Input, Button, RadioGroup, Radio, Dialog } from 'element-ui'
+// import { getPushCount, getRecommend } from '@/api/modules'
+import { debounce } from 'lodash'
+/* eslint-disable */
+Array.prototype.indexOf = function (val) {
+  for (var i = 0; i < this.length; i++) {
+    if (this[i] === val) return i
+  }
+  return -1
+}
+Array.prototype.remove = function (val) {
+  var index = this.indexOf(val)
+  if (index > -1) {
+    this.splice(index, 1)
+  }
+}
+/* eslint-disable */
+export default {
+  name: 'key-config',
+  props: {
+    datas: Array,
+    title: String,
+    keywords: Object,
+    className: '',
+    classIndex: Number,
+    keyIndex: Number,
+    getPushCount: Function,
+    getRecommend: Function
+  },
+  components: {
+    [Input.name]: Input,
+    [Button.name]: Button,
+    [RadioGroup.name]: RadioGroup,
+    [Radio.name]: Radio,
+    [Dialog.name]: Dialog
+  },
+  data() {
+    return {
+      // 当前输入框的数据
+      cur: {
+        classify: '',
+        key: '',
+        appendkey: '',
+        notkey: '',
+        matchway: 1
+      },
+      dialog: {
+        limit: false,
+        repeat: false
+      },
+      pushCount: 0,
+      showNotWay: false, // 是否展示添加排除词输入框
+      sameWordsList: [] // 相似推荐数据
+    }
+  },
+  computed: {
+    keyDisabled() {
+      let reg = /^\s+$/g //纯空格
+      if (!this.cur.key || reg.test(this.cur.key)) {
+        return true
+      } else {
+        return false
+      }
+    },
+    // 匹配信息过少提示
+    littleTip() {
+      return this.pushCount < 30 && this.cur.key !== '' && this.getRecommend
+    }
+  },
+  watch: {
+    className: function (newVal) {
+      this.className = newVal
+      this.getPropsData()
+    }
+  },
+  mounted() {
+    this.getPropsData()
+  },
+  methods: {
+    getPropsData() {
+      if (this.keywords) {
+        if (this.keywords.appendkey) {
+          this.cur.key =
+            this.keywords.key.join(' ') +
+            ' ' +
+            this.keywords.appendkey.join(' ')
+        } else {
+          this.cur.key = this.keywords.key.join(' ')
+        }
+        if (this.keywords.notkey && this.keywords.notkey.length > 0) {
+          this.showNotWay = true
+          this.cur.notkey = this.keywords.notkey
+            ? this.keywords.notkey.join(' ')
+            : null
+        }
+        if (this.keywords.matchway) {
+          this.cur.matchway = 1
+        } else {
+          this.cur.matchway = 0
+        }
+      }
+      if (this.className) {
+        this.cur.classify = this.className
+      }
+      if (this.title === '修改') {
+        this.getPushCountFn()
+      }
+    },
+    // 关键词推荐数量查询
+    keywordsInput() {
+      if (this.cur.key) {
+        setTimeout(() => {
+          this.getRecommendFn()
+        }, 800)
+        setTimeout(() => {
+          this.getPushCountFn()
+        }, 2000)
+      }
+    },
+    // 关键词输入框失去焦点 查询推送数量
+    keywordsBlur() {
+      if (this.cur.key) {
+        this.getPushCountFn()
+      }
+    },
+    // 排除词输入框失去焦点 查询推送数量
+    notKeyBlur() {
+      if (this.cur.notkey) {
+        this.getPushCountFn()
+      }
+    },
+    // 关键词近3个月推送数量查询
+    getPushCountFn: debounce(function () {
+      if (!this.cur.key) return
+      if (this.getPushCount) {
+        this.getPushCount({
+          key: this.cur.key,
+          notkey: this.cur.notkey,
+          matchway: this.cur.matchway
+        }).then((res) => {
+          this.pushCount = res.count
+        })
+      }
+    }, 500),
+    // 获取相似项目推荐
+    getRecommendFn: debounce(function () {
+      if (!this.getRecommend) {
+        return
+      }
+      this.getRecommend({
+        count: 20,
+        value: this.cur.key
+      }).then((res) => {
+        this.sameWordsList = []
+        const arrList = []
+        if (res.length > 0) {
+          res.forEach((item) => {
+            item.selected = false
+          })
+          const m = this.cur.key.split(' ')
+          m.forEach((v) => {
+            res.forEach((s) => {
+              if (v === s.word) {
+                s.selected = true
+              }
+            })
+          })
+        }
+        this.sameWordsList = res || []
+      })
+    }, 500),
+    chooseMatchWay() {
+      this.getPushCountFn()
+    },
+    // 添加订阅推荐到输入框
+    addSameItem(e, item) {
+      item.selected = !item.selected
+      const m = this.cur.key.split(' ')
+      if (item.selected) {
+        m.push(item.word)
+        this.cur.key = m.join(' ')
+      } else {
+        m.remove(item.word)
+        this.cur.key = m.join(' ')
+      }
+      setTimeout(() => {
+        this.getPushCountFn()
+      }, 2000)
+    },
+    getNotKeyArr() {
+      const notKey = this.cur.notkey.trim()
+      if (!notKey) return []
+      return notKey.split(/\s+/)
+    },
+    // 提交关键词
+    submitKeywords() {
+      const isRepeat = this.getRepeatFn()
+      if (isRepeat) {
+        this.dialog.repeat = true
+        return
+      }
+      const params = {
+        data: {
+          appendkey: this.cur.appendkey
+            ? this.cur.appendkey.split(/\s+/)
+            : null,
+          key: this.cur.key.split(/\s+/),
+          matchway: this.cur.matchway,
+          notkey: this.getNotKeyArr(),
+          updatetime: parseInt(Date.now() / 1000)
+        },
+        keyIndex: this.keyIndex,
+        classifyName: this.cur.classify,
+        classIndex: this.classIndex
+      }
+      if (this.title === '修改') {
+        this.$emit('update', Object.assign({ state: 'edit' }, params))
+      } else {
+        this.$emit('update', Object.assign({ state: 'add' }, params))
+      }
+      this.$nextTick(() => {
+        this.$emit('closeForm', {
+          keywords: this.keywords,
+          classIndex: this.classIndex,
+          keyIndex: this.keyIndex
+        })
+      })
+    },
+    // 判断附加词排除词输入框是否有空
+    inputIsEmpty(arr) {
+      return arr.some((v) => !v)
+    },
+    getRepeatFn() {
+      const datas = JSON.parse(JSON.stringify(this.datas)) || []
+      let newArr = []
+      datas.forEach((v) => {
+        v.a_key.forEach((s) => {
+          newArr.push(s)
+        })
+      })
+      const arr = this.cur.key.split(/\s+/)
+      const keyArr = arr[0].split(',')
+      const appendArr = arr.length > 1 ? arr.slice(1) : null
+      const notKeyArr = this.cur.notkey ? this.cur.notkey.split(',') : null
+      let isRepeat
+      for (let i = 0; i < newArr.length; i++) {
+        const v = newArr[i]
+        v.appendkey = v.appendkey && v.appendkey.length > 0 ? v.appendkey : null
+        v.notkey = v.notkey && v.notkey.length > 0 ? v.notkey : null
+        const rKey = JSON.stringify(v.key) === JSON.stringify(keyArr)
+        const rAppend =
+          JSON.stringify(v.appendkey) === JSON.stringify(appendArr)
+        const rNot = JSON.stringify(v.notkey) === JSON.stringify(notKeyArr)
+        const rWay = v.matchway === this.cur.matchway
+        if (rKey && rAppend && rNot && rWay) {
+          isRepeat = rKey && rAppend && rNot && rWay
+          break
+        }
+      }
+      return isRepeat
+    },
+    // 获取所有关键词的key的属性,并返回一个数组(主要用于判断添加关键词是否重复)
+    getKeyTotalArray() {
+      if (!this.datas) return
+      const keysArr = []
+      this.datas.forEach((s) => {
+        if (s && s.a_key && Array.isArray(s.a_key)) {
+          s.a_key.forEach((v) => {
+            if (Array.isArray(v.key)) {
+              keysArr.push(v.key.toString().replace(',', ' '))
+            } else {
+              keysArr.push(v.key)
+            }
+          })
+        }
+      })
+      return keysArr
+    },
+    // 判断关键词出现的个数
+    getRepeatCounts(arr, value) {
+      arr.reduce((a, v) => {
+        v === value ? a + 1 : a + 0
+      }, 0)
+    },
+    cancelEdit() {
+      this.$emit('closeForm', {
+        keywords: this.keywords,
+        classIndex: this.classIndex,
+        keyIndex: this.keyIndex
+      })
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.edit-form {
+  margin-top: 20px;
+  .classify-title {
+    padding-bottom: 20px;
+    .icon-edit,
+    .icon-delete {
+      display: inline-block;
+      width: 16px;
+      height: 16px;
+      background-repeat: no-repeat;
+      background-position: center center;
+      cursor: pointer;
+      &::before {
+        content: '' !important;
+      }
+    }
+    .icon-edit {
+      margin: 0 10px;
+      background-image: url('../../assets/images/icon/icon-edit.png');
+      background-size: contain;
+    }
+    .icon-delete {
+      background-image: url('../../assets/images/icon/icon-delete.png');
+      background-size: contain;
+    }
+  }
+  .input-box {
+    margin-top: 24px;
+    padding: 20px 30px;
+    background: #f6f7f9;
+    border-radius: 8px;
+  }
+  .input-box-title {
+    margin-bottom: 16px;
+    font-size: 16px;
+    text-align: center;
+    color: #1d1d1d;
+  }
+  .item {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 16px;
+    &.no-center {
+      align-items: unset;
+    }
+  }
+  .item-label {
+    margin-right: 8px;
+    min-width: 78px;
+    height: 40px;
+    color: #1d1d1d;
+    font-size: 14px;
+    line-height: 40px;
+    text-align: right;
+  }
+  .no-center {
+    .item-label {
+      line-height: 20px;
+    }
+  }
+  .item-label-required:before {
+    content: '*';
+    color: #f56c6c;
+    margin-right: 2px;
+  }
+  .recommend {
+    padding: 16px;
+    background: #fff;
+    border-radius: 4px;
+    .recommend-title {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      color: #686868;
+      font-size: 14px;
+      .batch-btn {
+        color: #2cb7ca;
+        font-size: 14px;
+        cursor: pointer;
+      }
+    }
+    .recommend-main {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      .r-list {
+        padding: 0 8px;
+        margin: 12px 12px 0 0;
+        height: 22px;
+        line-height: 22px;
+        background: #f5f6f7;
+        border-radius: 4px;
+        color: #1d1d1d;
+        font-size: 14px;
+        cursor: pointer;
+      }
+      .active {
+        background: #2cb7ca;
+        color: #fff;
+      }
+    }
+  }
+  .add-words-btn {
+    width: 352px;
+    height: 40px;
+    line-height: 40px;
+    border: 1px dashed #2cb7ca;
+    border-radius: 6px;
+    background: #fff;
+    color: #2cb7ca;
+    text-align: center;
+    cursor: pointer;
+    font-size: 14px;
+  }
+  .little-tips {
+    margin-top: 20px;
+    height: 32px;
+    line-height: 32px;
+    background: rgba(255, 159, 64, 0.1);
+    border-radius: 4px;
+    color: #ff9f40;
+    font-size: 13px;
+    text-align: center;
+  }
+  .item-value {
+    flex: 1;
+  }
+  .custom-long-input {
+    width: 352px;
+  }
+  .custom-short-input {
+    width: 170px;
+  }
+  .item-other {
+    display: flex;
+    justify-content: center;
+    margin-bottom: 10px;
+  }
+  .item-other-value {
+    margin-top: 8px;
+  }
+  .math-tips {
+    margin-top: 4px;
+    font-size: 12px;
+    color: #999999;
+    line-height: 20px;
+  }
+  .radio-item {
+    margin-bottom: 10px;
+  }
+  .item-keywords-value {
+    display: flex;
+    justify-content: space-between;
+  }
+  .add-tag {
+    width: 170px;
+    height: 40px;
+    line-height: 40px;
+    color: #2cb7ca;
+    border-radius: 6px;
+    text-align: center;
+    cursor: pointer;
+    color: #2cb7ca;
+    border: 1px dashed #2cb7ca;
+  }
+  .add-word-list {
+    margin-bottom: 6px;
+  }
+  .btn-groups {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-top: 20px;
+  }
+  .confirm-btn {
+    display: block;
+    width: 240px;
+    height: 46px;
+    line-height: 46px;
+    font-size: 16px;
+    background: #2cb7ca;
+    border-radius: 6px;
+    border-width: 0;
+    color: #fff;
+    cursor: pointer;
+    &:disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+    }
+  }
+  .cancle-btn {
+    display: block;
+    width: 240px;
+    height: 46px;
+    margin-left: 20px;
+    line-height: 46px;
+    background: #fff;
+    border: 1px solid #2cb7ca;
+    color: #2cb7ca;
+    border-radius: 6px;
+    text-align: center;
+    cursor: pointer;
+  }
+  .keywords-help {
+    width: 412px;
+    margin-top: 20px;
+    font-size: 12px;
+    color: #999999;
+    line-height: 20px;
+    text-align: justify;
+  }
+  .radio-item {
+    margin-bottom: 18px;
+  }
+}
+// element-ui样式修改
+::v-deep {
+  .el-input__inner,
+  .el-textarea__inner {
+    font-size: 14px;
+    color: #1d1d1d;
+    border-color: #ececec;
+  }
+  .el-textarea__inner {
+    padding: 8px 15px;
+  }
+  .el-input__inner:hover,
+  .el-textarea__inner:hover {
+    border-color: #ececec;
+  }
+  .el-input__inner:focus,
+  .el-textarea__inner:focus {
+    border-color: #2cb7ca;
+  }
+  .el-radio {
+    color: #1d1d1d;
+    font-size: 14px;
+  }
+  .el-radio__inner {
+    width: 20px;
+    height: 20px;
+  }
+  .el-radio__input.is-checked .el-radio__inner {
+    border: 0;
+    background: transparent;
+    width: 20px;
+    height: 20px;
+    background: url('../../assets/images/icon/icon-checked.png') no-repeat center center;
+    background-size: contain;
+  }
+  .el-radio__inner::after {
+    background: transparent;
+  }
+  .el-radio__input.is-checked + .el-radio__label {
+    color: #1d1d1d;
+  }
+  .el-radio__inner:hover {
+    border-color: #ececec;
+  }
+}
+</style>

+ 806 - 0
packages/pc-ui/src/packages/keywords/keywords-list.vue

@@ -0,0 +1,806 @@
+<template>
+  <div class="classify" id="auxiliaryFindRange">
+    <div class="fixed-top-group">
+      <div
+        class="classify-list"
+        v-for="(item, index) in newWordsList"
+        :key="'top-1' + index"
+      >
+        <div
+          style="display: none"
+          class="classify-title flex-r-c sb"
+          @click="goThisTop(index)"
+          :data-diy-sticky-mapping="'sticky-' + index"
+        >
+          <div class="flex-r-c">
+            <span class="title-text">{{ item.s_item }}</span>
+            <span
+              class="icon-edit"
+              @click="editClassFn(item.s_item, index)"
+            ></span>
+            <span
+              class="icon-delete"
+              @click="deleteClassFn(item, index)"
+            ></span>
+          </div>
+          <div class="flex-r-c right">
+            <el-button
+              type="primary"
+              class="add-classfily"
+              icon="el-icon-plus"
+              @click="addNewKeyword(item, index)"
+              >新增关键词组</el-button
+            >
+            <div
+              class="flex-r-c center list-item-opened"
+              @click="slideToggle(item)"
+            >
+              <span>{{ item.opened ? '收起' : '展开' }}</span>
+              <i
+                :class="item.opened ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"
+              ></i>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div
+      class="classify-list"
+      v-for="(item, index) in newWordsList"
+      :key="'1' + index"
+    >
+      <div
+        class="classify-title flex-r-c sb"
+        :data-diy-sticky-origin="'sticky-' + index"
+      >
+        <div class="flex-r-c">
+          <span class="title-text">{{ item.s_item }}</span>
+          <span
+            class="icon-edit"
+            @click="editClassFn(item.s_item, index)"
+          ></span>
+          <span class="icon-delete" @click="deleteClassFn(item, index)"></span>
+        </div>
+        <div class="flex-r-c right">
+          <div
+            class="flex-r-c center list-item-opened"
+            @click="slideToggle(item)"
+          >
+            <span>{{ item.opened ? '收起' : '展开' }}</span>
+            <i
+              :class="item.opened ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"
+            ></i>
+          </div>
+        </div>
+      </div>
+      <el-collapse-transition>
+        <div class="classify-content" v-show="item.opened">
+          <div
+            class="add-words-box"
+            @click="addNewKeyword(item, index)"
+            v-if="!item.showForm"
+            key="add"
+          >
+            +新增关键词组
+          </div>
+          <div style="width: 100%" v-else key="add2">
+            <Edit
+              :getPushCount="getPushCount"
+              :getRecommend="getRecommend"
+              :datas="newWordsList"
+              :className="item.s_item"
+              title="新增"
+              :classIndex="index"
+              :keyIndex="item.a_key.length"
+              @closeForm="onCloseForm"
+              @update="getUpdateKey"
+            >
+            </Edit>
+          </div>
+          <div v-for="(v, i) in item.a_key" :key="'2' + i" style="width: 100%">
+            <Edit
+              :datas="newWordsList"
+              title="修改"
+              :className="item.s_item"
+              :keywords="v"
+              :classIndex="index"
+              :keyIndex="i"
+              @closeForm="onCloseForm"
+              @update="getUpdateKey"
+              v-if="v.showForm"
+              key="edit"
+            >
+            </Edit>
+            <div class="words-list" v-else key="edit2">
+              <div class="list-left yellow-box" v-if="v.matchway">模糊</div>
+              <div class="list-left blue-box" v-else>精准</div>
+              <div class="list-middle">
+                <div class="list-keywords" v-if="v.appendkey" key="append">
+                  {{ v.key.join(' ') + ' ' + v.appendkey.join(' ') }}
+                </div>
+                <div class="list-keywords" v-else key="append2">
+                  {{ v.key.join(' ') }}
+                </div>
+                <p class="list-notkey" v-if="v.notkey && v.notkey.length > 0">
+                  排除词: {{ v.notkey.join(' ') }}
+                </p>
+              </div>
+              <div class="list-right">
+                <span
+                  class="icon-edit"
+                  @click="editKeyFn(item, v, index, i)"
+                ></span>
+                <span class="icon-delete" @click="deleteKeyFn(index, i)"></span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </el-collapse-transition>
+    </div>
+    <!-- 修改分类dialog -->
+    <el-dialog
+      custom-class="sub-dialog small-dialog"
+      :visible.sync="dialog.editClass"
+      :close-on-click-modal="false"
+      :show-close="false"
+      center
+      width="460px"
+    >
+      <KeyCard
+        @onCancel="dialog.editClass = false"
+        @onConfirm="confirmEditClassFn"
+      >
+        <div slot="header">修改关键词分类</div>
+        <div class="class-edit-content">
+          <div class="item">
+            <div class="item-label">关键词分类:</div>
+            <div class="item-value">
+              <el-input
+                class="custom-long-input"
+                v-model.trim="props.className"
+                maxlength="20"
+                placeholder="请输入关键词分类"
+              ></el-input>
+            </div>
+          </div>
+        </div>
+      </KeyCard>
+    </el-dialog>
+    <!-- 删除分类dialog -->
+    <el-dialog
+      custom-class="sub-dialog small-dialog"
+      :visible.sync="dialog.delClass"
+      :close-on-click-modal="false"
+      :show-close="false"
+      center
+      top="30vh"
+      width="380px"
+    >
+      <div class="delete-class">
+        <div class="delete-class-header">删除关键词分类</div>
+        <div class="delete-class-content">{{ props.className }}</div>
+        <div class="delete-class-footer">
+          <el-button
+            type="primary"
+            class="confirm"
+            @click="confirmDeleteClassFn"
+            >删除</el-button
+          >
+          <el-button class="cancel" @click="dialog.delClass = false"
+            >取消</el-button
+          >
+        </div>
+      </div>
+    </el-dialog>
+    <!-- 删除关键词dialog -->
+    <el-dialog
+      custom-class="sub-dialog small-dialog"
+      :visible.sync="dialog.delKey"
+      :close-on-click-modal="false"
+      :show-close="false"
+      top="30vh"
+      center
+      width="380px"
+    >
+      <div class="delete-class">
+        <div class="delete-class-header">删除关键词组</div>
+        <div class="delete-class-content">确定删除该关键词组吗?</div>
+        <div class="delete-class-footer">
+          <el-button type="primary" class="confirm" @click="confirmDeleteKeyFn"
+            >确定</el-button
+          >
+          <el-button class="cancel" @click="dialog.delKey = false"
+            >取消</el-button
+          >
+        </div>
+      </div>
+    </el-dialog>
+    <!-- 删除分类dialog -->
+    <el-dialog
+      custom-class="small-dialog"
+      title="删除关键词分类"
+      :visible.sync="dialog.notDelClass"
+      width="380px"
+      center
+      :show-close="false"
+    >
+      <p class="dialog-text">
+        该关键词分类下存在关键词组,无法删除,请先删除该分类下的关键词组
+      </p>
+      <span slot="footer" class="dialog-footer">
+        <el-button
+          class="know-btn"
+          type="primary"
+          @click="dialog.notDelClass = false"
+          >我知道了</el-button
+        >
+      </span>
+    </el-dialog>
+    <!-- 关键词添加超限提示 -->
+    <el-dialog
+      custom-class="small-dialog"
+      title="新增关键词组"
+      :visible.sync="dialog.limit"
+      width="380px"
+      center
+      :show-close="false"
+    >
+      <p class="dialog-text">您的关键词组数量已达300组,无法继续添加</p>
+      <span slot="footer" class="dialog-footer">
+        <el-button class="know-btn" type="primary" @click="dialog.limit = false"
+          >我知道了</el-button
+        >
+      </span>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import { Tooltip, Dialog, Input, Button } from 'element-ui'
+import CollapseTransition from 'element-ui/lib/transitions/collapse-transition'
+import KeyCard from './selector-card.vue'
+import Edit from './keywords-edit.vue'
+
+export default {
+  name: 'keywords-list',
+  components: {
+    [Tooltip.name]: Tooltip,
+    [Dialog.name]: Dialog,
+    [Input.name]: Input,
+    [Button.name]: Button,
+    KeyCard,
+    Edit,
+    [CollapseTransition.name]: CollapseTransition
+  },
+  props: {
+    getPushCount: Function,
+    getRecommend: Function,
+    list: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    maxCount: Number
+  },
+  data() {
+    return {
+      dialog: {
+        editClass: false, // 修改分类弹框
+        delClass: false, // 删除分类弹框
+        editKey: false, // 修改关键词弹框
+        delKey: false, // 删除关键词
+        notDelClass: false, // 不能删除分类弹框
+        limit: false // 关键词设置超出上限弹框
+      },
+      // 传给dialog子组件的数据
+      props: {
+        ways: '', // 编辑还是新增
+        classIndex: null, // 分类下标
+        className: '', // 分类名
+        keyIndex: null, // 关键词下标
+        key: [], // 关键词
+        notkey: [], // 排除词
+        appendkey: [] // 附加词
+      },
+      newWordsList: this.list // 将父组件传来的数据赋值给该变量,
+    }
+  },
+  watch: {
+    // list: function (newVal) {
+    //   if (newVal) {
+    //     const oldList = JSON.parse(JSON.stringify(this.newWordsList))
+    //     this.newWordsList = newVal
+    //     this.formatDataList(oldList)
+    //   }
+    // }
+  },
+  mounted() {
+    this.formatDataList()
+  },
+  methods: {
+    getUpdateKey(params) {
+      const { state, classIndex, data, keyIndex } = params
+      const list = this.newWordsList
+      list.forEach((v, index) => {
+        if (index === classIndex) {
+          if (v.a_key) {
+            if (state === 'edit') {
+              v.a_key.forEach((s, j) => {
+                if (keyIndex === j) {
+                  for (const key in data) {
+                    s[key] = data[key]
+                  }
+                }
+              })
+            } else {
+              v.a_key.push(data)
+            }
+          }
+        }
+      })
+      console.log(list, 'update-list')
+      this.$emit('update', list)
+    },
+    // 当前分类下有无正在编辑的
+    getItemCurEdit(item) {
+      const s = item.a_key.some((v) => {
+        return v.showForm
+      })
+      return s || item.showForm
+    },
+    slideToggle(item) {
+      const s = this.getItemCurEdit(item)
+      if (item.opened) {
+        if (s) return this.$toast('收起失败,请先保存或取消正在操作的关键词组')
+      }
+      item.opened = !item.opened
+      this.$forceUpdate()
+    },
+    goThisTop(index) {
+      const goTop =
+        $(
+          '#auxiliaryFindRange *[data-diy-sticky-origin="sticky-' + index + '"]'
+        ).offset().top -
+        $('#public-nav').outerHeight() -
+        20
+      $(window).scrollTop(goTop)
+    },
+    sortData(arr) {
+      return arr.sort((a, b) => {
+        return b.updatetime - a.updatetime
+      })
+    },
+    // 处理数据 添加showForm字段
+    formatDataList(oldData) {
+      const lists = this.newWordsList
+      this.sortData(lists)
+      lists.forEach((v, index) => {
+        if (v) {
+          if (
+            !(typeof v.opened === 'string' || typeof v.opened === 'boolean')
+          ) {
+            v.opened = true
+            // TODO 生成索引缓存结果减少遍历优化性能
+            if (oldData && Array.isArray(oldData)) {
+              const findResult = oldData.filter((s) => s.s_item === v.s_item)
+              if (
+                findResult.length &&
+                Object.prototype.hasOwnProperty.call(findResult[0], 'opened')
+              ) {
+                v.opened = findResult[0].opened
+              }
+            }
+          }
+          if (v.a_key && v.a_key.length > 0) {
+            this.sortData(v.a_key)
+          } else {
+            v.a_key = []
+            // if (lists.length === 1) {
+            //   v.showForm = true
+            // }
+          }
+        }
+      })
+      this.newWordsList = lists
+    },
+    // 打开修改关键词分类弹框
+    editClassFn(name, index) {
+      const t = this.getCurEdit()
+      if (t) return this.$toast('请先保存或取消正在操作的关键词组')
+      this.dialog.editClass = true
+      this.props.className = name
+      this.props.classIndex = index
+    },
+    // 确认修改分类
+    confirmEditClassFn() {
+      const data = this.newWordsList
+      const classArr = this.getClassArray()
+      if (classArr.indexOf(this.props.className) > -1) {
+        return this.$message({
+          type: 'warning',
+          message: '分类名不能重复'
+        })
+      } else if (this.props.className === '') {
+        return this.$message({
+          type: 'warning',
+          message: '分类名不能为空'
+        })
+      }
+      data.forEach((v, i) => {
+        if (this.props.classIndex === i) {
+          v.s_item = this.props.className
+          v.updatetime = parseInt(Date.now() / 1000)
+        }
+      })
+      this.$nextTick(() => {
+        this.dialog.editClass = false
+        this.$emit('update', data)
+        this.$forceUpdate()
+      })
+    },
+    // 新增关键词 打开编辑表单
+    addNewKeyword(item, index) {
+      const t = this.getCurEdit()
+      if (t) return this.$toast('请先保存或取消正在操作的关键词组')
+      const isCan = this.getIsAdd()
+      if (isCan) {
+        this.dialog.limit = true
+        return
+      }
+      item.showForm = true
+      item.opened = true
+      this.$forceUpdate()
+    },
+    // 打开删除关键词分类弹框
+    deleteClassFn(item, index) {
+      if (item.a_key && item.a_key.length > 0) {
+        this.dialog.notDelClass = true
+        return
+      }
+      this.props.classIndex = null
+      this.dialog.delClass = true
+      this.props.className = item.s_item
+      this.props.classIndex = index
+    },
+    // 确认删除分类
+    confirmDeleteClassFn() {
+      const list = this.newWordsList
+      list.splice(this.props.classIndex, 1)
+      this.$nextTick(() => {
+        this.dialog.delClass = false
+      })
+    },
+    // 编辑单个关键词
+    editKeyFn(item, v, index, i) {
+      const t = this.getCurEdit()
+      if (t) return this.$toast('请先保存或取消正在操作的关键词组')
+      this.clearPropsData()
+      this.props.className = item.s_item
+      this.props.key = v.key
+      this.props.notkey = v.notkey
+      this.props.appendkey = v.appendkey
+      this.props.keyIndex = i
+      this.props.classIndex = index
+      v.showForm = true
+    },
+    // 打开删除关键词dialog
+    deleteKeyFn(index, i) {
+      this.props.classIndex = index
+      this.props.keyIndex = i
+      this.dialog.delKey = true
+    },
+    // 确定删除关键词组
+    confirmDeleteKeyFn() {
+      const data = this.newWordsList
+      console.log(data, this.props.classIndex, this.props.keyIndex)
+      data[this.props.classIndex].a_key.splice(this.props.keyIndex, 1)
+      this.dialog.delKey = false
+      this.$forceUpdate()
+    },
+    // 清空传值
+    clearPropsData() {
+      this.props.className = ''
+      this.props.classIndex = null
+      this.props.key = []
+      this.props.notkey = []
+      this.props.appendkey = []
+    },
+    // 获取所有分类名 用户判断添加分类是否重复
+    getClassArray() {
+      const data = this.newWordsList
+      const classArr = []
+      data.forEach((v) => {
+        if (v.s_item) {
+          classArr.push(v.s_item)
+        }
+      })
+      return classArr
+    },
+    // 添加了多少组关键词
+    addTotalCount() {
+      let count = 0
+      const data = this.newWordsList
+      data.forEach((v) => {
+        if (v && v.a_key) {
+          count += v.a_key.length
+        }
+      })
+      return count
+    },
+    // 判断是否超限
+    getIsAdd() {
+      const len = this.addTotalCount()
+      if (len >= this.maxCount) {
+        return true
+      } else {
+        return false
+      }
+    },
+    onCloseForm(data) {
+      if (!data) return
+      if (data.keywords) {
+        data.keywords.showForm = false
+        this.$forceUpdate()
+      } else {
+        this.newWordsList[data.classIndex].showForm = false
+        this.$forceUpdate()
+      }
+      this.formatDataList()
+    },
+    // 查询有无正在编辑的表单
+    getCurEdit() {
+      const list = this.newWordsList
+      let t
+      const s = list.some((v) => {
+        if (v.a_key) {
+          t = v.a_key.some((s) => {
+            return s.showForm
+          })
+        }
+        return v.showForm || t
+      })
+      return s
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.fixed-top-group {
+  position: fixed;
+  width: 1080px;
+  top: 0;
+  left: 50%;
+  z-index: 99;
+  transform: translateX(-50%);
+  @media only screen and (max-width: 1200px) {
+    background-color: red;
+    transform: unset;
+    left: 60px;
+  }
+  .classify-list {
+    margin-bottom: 0;
+  }
+  .classify-list .classify-title {
+    width: 100%;
+    position: absolute;
+    background: #fff;
+    padding: 9px 32px;
+    margin: 0;
+    box-shadow: 0px 0px 28px 0px rgba(0, 0, 0, 0.16);
+  }
+}
+.classify-list {
+  margin-bottom: 8px;
+  // border-radius: 8px;
+  .list-item-opened {
+    font-size: 14px;
+    font-family: Microsoft YaHei, Microsoft YaHei-Regular;
+    font-weight: 400;
+    text-align: left;
+    color: #686868;
+    text-shadow: 0px 0px 28px 0px rgba(0, 0, 0, 0.16);
+    cursor: pointer;
+    i {
+      color: #aaaaaa;
+      margin-left: 7px;
+    }
+  }
+  .add-classfily {
+    line-height: 22px;
+    background-color: transparent;
+    border: 1px solid #2cb7ca;
+    border-radius: 6px;
+    text-align: center;
+    color: #2cb7ca;
+    font-size: 14px;
+    cursor: pointer;
+    padding: 4px 16px;
+    box-sizing: border-box;
+    & + .list-item-opened {
+      margin-left: 16px;
+    }
+  }
+  .classify-title {
+    display: flex;
+    align-items: center;
+    padding: 9px 0;
+    .title-text {
+      font-size: 16px;
+      color: #1d1d1d;
+    }
+  }
+  .icon-edit,
+  .icon-delete {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    background-repeat: no-repeat;
+    background-position: center center;
+    cursor: pointer;
+    &::before {
+      content: '' !important;
+    }
+  }
+  .icon-edit {
+    margin: 0 10px;
+    background-image: url('../../assets/images/icon/icon-edit.png');
+    background-size: contain;
+  }
+  .icon-delete {
+    background-image: url('../../assets/images/icon/icon-delete.png');
+    background-size: contain;
+  }
+  .classify-content {
+    .edit-form {
+      margin-top: 0;
+    }
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    .add-words-box {
+      width: 100%;
+      height: 38px;
+      line-height: 38px;
+      border: 1px dashed #2cb7ca;
+      border-radius: 6px;
+      color: #2cb7ca;
+      font-size: 14px;
+      text-align: center;
+      cursor: pointer;
+    }
+    .words-list {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 12px 16px;
+      margin-top: 24px;
+      border: 1px solid #ececec;
+      border-radius: 8px;
+      box-sizing: border-box;
+      .list-left {
+        width: 40px;
+        height: 22px;
+        line-height: 22px;
+        margin-right: 12px;
+        font-size: 12px;
+        text-align: center;
+        box-sizing: border-box;
+      }
+      .yellow-box {
+        border: 1px solid #ff9f40;
+        color: #ff9f40;
+        border-radius: 4px;
+      }
+      .blue-box {
+        border: 1px solid #2cb7ca;
+        color: #2cb7ca;
+        border-radius: 4px;
+      }
+      .list-middle {
+        flex: 1;
+      }
+      .list-keywords {
+        color: #1d1d1d;
+        line-height: 22px;
+        font-size: 14px;
+      }
+      .list-addkey,
+      .list-notkey {
+        color: #686868;
+        line-height: 20px;
+        font-size: 12px;
+      }
+      .list-right {
+        margin-left: 15px;
+      }
+    }
+
+    .list {
+      padding-top: 10px;
+      margin-right: 10px;
+      margin-bottom: 10px;
+      width: 160px;
+      height: 80px;
+      box-sizing: border-box;
+      &:hover .list-edit {
+        transform: scaleY(1);
+        transition: transform 0.1s;
+      }
+      &:hover .list-box {
+        border: 1px solid #2cb7ca;
+        box-sizing: border-box;
+      }
+    }
+    :list:nth-child(6n) {
+      margin-right: 0;
+    }
+    .list-box {
+      position: relative;
+      display: flex;
+      padding: 10px 10px 10px 16px;
+      border: 1px solid #ececec;
+      border-radius: 9px;
+      box-sizing: border-box;
+      cursor: pointer;
+    }
+    .list-edit {
+      transform: scaleY(0);
+      transition: transform 0.1s;
+      position: absolute;
+      top: -40px;
+      left: 50%;
+      margin-left: -22px;
+      padding: 10px;
+      color: #fff;
+      background: #1d1d1d;
+      border-radius: 4px;
+      font-size: 12px;
+      cursor: pointer;
+      .tri-down {
+        position: absolute;
+        bottom: -6px;
+        left: 50%;
+        margin-left: -5px;
+        width: 0;
+        height: 0;
+        border-left: 6px solid transparent;
+        border-right: 6px solid transparent;
+        border-top: 6px solid #1d1d1d;
+      }
+    }
+    .list-value {
+      width: 124px;
+      font-size: 12px;
+      line-height: 20px;
+      color: #000;
+      text-overflow: ellipsis;
+      overflow: hidden;
+      white-space: nowrap;
+      p {
+        @extend .list-value;
+      }
+    }
+    .list-icon {
+      @extend .icon-delete;
+      width: 12px;
+      height: 12px;
+    }
+    .words-add {
+      width: 162px;
+      height: 80px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 24px;
+      border: 1px solid #ececec;
+      margin: 10px 0 0 0;
+      border-radius: 9px;
+      color: #c4c4c4;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 298 - 0
packages/pc-ui/src/packages/keywords/selector-card.vue

@@ -0,0 +1,298 @@
+<template>
+  <div
+    class="selector-card"
+    :class="{
+      's-card': cardType === 'card',
+      's-line': cardType === 'line'
+    }"
+  >
+    <div
+      class="selector-card-header"
+      :class="{ 's-header': cardType === 'line' }"
+    >
+      <slot name="header"></slot>
+    </div>
+    <div class="selector-card-content scrollbar">
+      <slot name="default"></slot>
+    </div>
+    <div class="selector-card-footer">
+      <slot name="footer">
+        <el-button
+          v-if="cardType === 'card'"
+          type="primary"
+          class="confirm"
+          :disabled="confirmDisabled"
+          @click="onConfirm"
+          >{{ confirmText }}</el-button
+        >
+        <el-button
+          v-if="cardType === 'card'"
+          class="cancel"
+          @click="onCancel"
+          >{{ cancelText }}</el-button
+        >
+      </slot>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Button } from 'element-ui'
+export default {
+  name: 'selector-card',
+  components: {
+    [Button.name]: Button
+  },
+  props: {
+    cardType: {
+      type: String,
+      default: 'card',
+      validator(value) {
+        return ['card', 'line'].indexOf(value) !== -1
+      }
+    },
+    confirmText: {
+      type: String,
+      default: '保存'
+    },
+    cancelText: {
+      type: String,
+      default: '取消'
+    },
+    confirmDisabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    onCancel() {
+      this.$emit('onCancel')
+    },
+    onConfirm() {
+      this.$emit('onConfirm')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.selector-card {
+  display: flex;
+  color: #1d1d1d;
+  background-color: #fff;
+  .selector-card-content {
+    position: relative;
+    display: flex;
+    flex: 1;
+
+    &::v-deep {
+      .fw-bold {
+        font-weight: bold;
+      }
+      // 子组件按钮公共样式
+      .j-button-item {
+        display: flex;
+        align-items: center;
+        margin: 6px 5px;
+        padding: 2px 6px;
+        line-height: 20px;
+        border-radius: 4px;
+        font-size: 14px;
+        text-align: center;
+        background-color: #fff;
+        border: 1px solid rgba(0, 0, 0, 0.05);
+        cursor: pointer;
+        &.global {
+          // s-card中的全部按钮使用
+          padding: 6px 8px;
+          height: 24px;
+          line-height: 24px;
+          font-weight: 700;
+          color: inherit;
+          border-color: rgba(0, 0, 0, 0.05);
+        }
+        &.all {
+          // s-line中的全部按钮使用
+          font-weight: 700;
+          border-color: transparent;
+        }
+        &.hover:hover {
+          color: #2abed1;
+        }
+        // 选中状态
+        &.active {
+          // 默认蓝色边框蓝色字体
+          color: #2abed1;
+          border-color: #2abed1;
+          &.bgc {
+            // 默认蓝色背景白色字体
+            color: #fff;
+            background-color: #2abed1;
+          }
+          &.bgc-opacity {
+            // 默认蓝色边框蓝色字体蓝色半透明背景
+            background-color: rgba(44, 183, 202, 0.1);
+          }
+        }
+      }
+
+      [class^='el-icon-'] {
+        transition: transform 0.2s ease;
+      }
+      .rotate180 {
+        transform: rotate(180deg);
+      }
+    }
+  }
+}
+
+.selector-card.s-card {
+  width: 460px;
+  height: 582px;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+  box-shadow: 0 0 28px rgb(0 0 0 / 16%);
+  border-radius: 5px;
+
+  .selector-card-header,
+  .selector-card-content,
+  .selector-card-footer {
+    width: 100%;
+  }
+
+  .selector-card-header {
+    padding-top: 30px;
+    margin: 0 0 40px;
+    font-size: 20px;
+    line-height: 26px;
+    text-align: center;
+    background: linear-gradient(180deg, #e0e0e0, #fff);
+    border-radius: 5px 5px 0 0;
+  }
+  .selector-card-content {
+    align-items: center;
+    overflow: hidden;
+  }
+  .selector-card-footer {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding-top: 20px;
+    padding-bottom: 40px;
+    ::v-deep {
+      .el-button--primary {
+        color: #2cb7ca;
+        background: none;
+        border-color: #2cb7ca;
+        &:hover {
+          color: #fff;
+          background-color: #2cb7ca;
+          &:disabled {
+            opacity: 0.6;
+          }
+        }
+      }
+      .el-button--default {
+        &:hover,
+        &:active {
+          color: #2cb7ca;
+          border-color: #2cb7ca;
+        }
+      }
+    }
+    .confirm,
+    .cancel {
+      padding: 10px 50px;
+      margin: 0 20px;
+    }
+  }
+
+  &::v-deep {
+    .j-button-item {
+      color: #606266;
+    }
+
+    .search-container {
+      margin: 20px auto;
+      width: 360px;
+      // 输入框公共样式
+      .el-input__inner {
+        padding-left: 42px;
+        background-color: #f7f7f7;
+        border-radius: 22px;
+      }
+      .el-input__prefix {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+      .el-icon-search {
+        margin-left: 6px;
+        font-size: 18px;
+      }
+    }
+
+    // 子组件卡片布局样式
+    .selector-content {
+      display: flex;
+      flex-direction: column;
+      margin: 0 auto;
+      width: 400px;
+      height: 100%;
+      border-radius: 5px;
+      border: 1px solid rgba(0, 0, 0, 0.05);
+    }
+
+    // 搜索框下的列表
+    .select-list {
+      position: relative;
+      flex: 1;
+      overflow-y: scroll;
+    }
+
+    // indexBar样式
+    .index-anchor {
+      margin-top: 10px;
+      padding: 0 20px;
+      height: 30px;
+      line-height: 30px;
+      background-color: #f5f5fb;
+      text-align: left;
+    }
+  }
+}
+
+.selector-card.s-line {
+  padding: 12px 40px;
+  .s-header {
+    line-height: 36px;
+  }
+  .selector-card-header {
+    margin-right: 10px;
+    min-width: 100px;
+  }
+  .selector-content {
+    position: relative;
+  }
+  ::v-deep {
+    .j-button-item {
+      border-color: transparent;
+    }
+    .action-button.show-more {
+      display: flex;
+      align-items: center;
+      position: absolute;
+      top: 8px;
+      right: 0;
+      font-size: 12px;
+      line-height: 20px;
+      color: #686868;
+      cursor: pointer;
+      .action-text {
+        margin-right: 4px;
+      }
+    }
+  }
+}
+</style>

+ 56 - 0
packages/pc-ui/src/packages/toast/Toast.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="wrap" v-if="showWrap" :class="showContent ? 'fadein' : 'fadeout'">
+    {{ text }}
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Toast',
+  data() {
+    return {
+      text: '',
+      showWrap: true,
+      showContent: true
+    }
+  }
+}
+</script>
+
+<style scoped>
+.wrap {
+  position: fixed;
+  left: 50%;
+  top: 50%;
+  background: rgba(0, 0, 0, 0.65);
+  padding: 16px 32px;
+  border-radius: 8px;
+  transform: translate(-50%, -50%);
+  color: #fff;
+  font-size: 16px;
+  z-index: 9999;
+}
+.fadein {
+  animation: animate_in 0.25s;
+}
+.fadeout {
+  animation: animate_out 0.25s;
+  opacity: 0;
+}
+@keyframes animate_in {
+  0% {
+    opacity: 0;
+  }
+  100% {
+    opacity: 1;
+  }
+}
+@keyframes animate_out {
+  0% {
+    opacity: 1;
+  }
+  100% {
+    opacity: 0;
+  }
+}
+</style>

+ 60 - 0
packages/pc-ui/src/packages/toast/index.js

@@ -0,0 +1,60 @@
+import toastComponent from './Toast.vue'
+
+let ToastHistory = {}
+
+const install = function (Vue) {
+  if (install.installed) return
+  if (Vue.prototype.$toast) return
+
+  const ToastConstructor = Vue.extend(toastComponent)
+
+  // 定义弹出组件的函数 接收2个参数, 要显示的文本 和 显示时间
+  function showToast(text, duration = 2000) {
+    if (ToastHistory.el) {
+      ToastHistory.destory()
+    }
+    // 实例化一个 toast.vue
+    const toastDom = new ToastConstructor({
+      el: document.createElement('div'),
+      data() {
+        return {
+          text: text,
+          showWrap: true,
+          showContent: true
+        }
+      }
+    })
+    // 把 实例化的 toast.vue 添加到 body 里
+    try {
+      this.$root.$el.appendChild(toastDom.$el)
+    } catch (error) {
+      document.body.appendChild(toastDom.$el)
+    }
+    return new Promise((resolve, reject) => {
+      // 提前 250ms 执行淡出动画(因为我们再css里面设置的隐藏动画持续是250ms)
+      const tFn1 = setTimeout(() => {
+        toastDom.showContent = false
+      }, duration - 250)
+      // 过了 duration 时间后隐藏整个组件
+      const tFn2 = setTimeout(() => {
+        toastDom.showWrap = false
+        resolve()
+      }, duration)
+
+      ToastHistory = {
+        el: toastDom.$el,
+        destory: () => {
+          clearTimeout(tFn1)
+          clearTimeout(tFn2)
+          toastDom.$el.remove()
+        }
+      }
+    })
+  }
+
+  Vue.prototype.$toast = showToast
+}
+
+export default {
+  install
+}

+ 31 - 0
packages/pc-ui/vite.config.js

@@ -0,0 +1,31 @@
+import { resolve } from 'path'
+import { defineConfig } from 'vite'
+import legacy from '@vitejs/plugin-legacy'
+import vue2 from '@vitejs/plugin-vue2'
+
+export default defineConfig({
+  plugins: [
+    vue2(),
+    legacy({
+      targets: ['ie >= 11'],
+      additionalLegacyPolyfills: ['regenerator-runtime/runtime']
+    })
+  ],
+  resolve: {
+    alias: [
+      // {
+      //   find: /^~/,
+      //   replacement: "",
+      // },
+      {
+        find: '~@',
+        replacement: resolve(__dirname, 'src')
+      },
+      {
+        find: '@',
+        replacement: resolve(__dirname, 'src')
+      }
+    ],
+    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
+  }
+})