Browse Source

feat: seo优化
1. 将前端展示页面进行了seo优化
2. 产品编辑功能
3. 产品展示效果优化
4. 类型排序功能

kindring 1 year ago
parent
commit
2d6719f2f4
53 changed files with 3496 additions and 161 deletions
  1. 596 0
      assets/antd_tabs.css
  2. 0 0
      assets/icons/svg/base-setting.svg
  3. 151 0
      assets/public.css
  4. 1 0
      assets/publicBanner.css
  5. 1 1
      components/banner/productBanner.vue
  6. 4 1
      components/header/menuDrops/menuDrop.vue
  7. 3 3
      components/newsTypes.vue
  8. 348 0
      components/product/productInfo.vue
  9. 12 4
      components/public/form/inputRow.vue
  10. 5 1
      components/public/imageViewer.vue
  11. 16 5
      components/showTypes.vue
  12. 1 1
      components/solutionTypes.vue
  13. 11 0
      map/adminSideBar.js
  14. 15 0
      map/apiMap.js
  15. 12 0
      map/langMap.js
  16. 93 0
      pages/manger/base.vue
  17. 269 0
      pages/manger/base/index.vue
  18. 19 0
      pages/manger/index/showing.vue
  19. 4 3
      pages/manger/news/add.vue
  20. 17 6
      pages/manger/news/info.vue
  21. 1 1
      pages/manger/news/type.vue
  22. 634 0
      pages/manger/product/add.vue
  23. 84 0
      pages/manger/product/edit.vue
  24. 4 1
      pages/manger/product/index.vue
  25. 140 0
      pages/manger/product/info.vue
  26. 52 5
      pages/news/_type.vue
  27. 45 3
      pages/news/index.vue
  28. 41 8
      pages/news/info/_type.vue
  29. 60 1
      pages/news/info/index.vue
  30. 21 15
      pages/product/_type.vue
  31. 45 18
      pages/product/index.vue
  32. 26 4
      pages/product/info/_type.vue
  33. 105 39
      pages/product/info/index.vue
  34. 49 2
      pages/solution/_type.vue
  35. 45 9
      pages/solution/index.vue
  36. 34 2
      pages/solution/info/_type.vue
  37. 59 1
      pages/solution/info/index.vue
  38. 54 2
      server/control/c_base.js
  39. 1 1
      server/control/c_solution.js
  40. 100 9
      server/control/product.js
  41. 61 1
      server/database/d_base.js
  42. 7 2
      server/database/d_news.js
  43. 100 3
      server/database/d_product.js
  44. 8 3
      server/database/d_solution.js
  45. 1 0
      server/index.js
  46. 36 0
      server/router/r_base.js
  47. 63 1
      server/router/r_product.js
  48. 1 1
      server/tools/searchSql.js
  49. 26 2
      store/index.js
  50. 5 1
      until/form/rules.js
  51. 7 1
      until/formTool.js
  52. 1 0
      until/time.js
  53. 2 0
      until/unescapeHtml.js

+ 596 - 0
assets/antd_tabs.css

@@ -0,0 +1,596 @@
+/* stylelint-disable at-rule-empty-line-before,at-rule-name-space-after,at-rule-no-unknown */
+/* stylelint-disable no-duplicate-selectors */
+/* stylelint-disable */
+/* stylelint-disable declaration-bang-space-before,no-duplicate-selectors,string-no-newline */
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-nav-container {
+  height: 40px;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-ink-bar {
+  visibility: hidden;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab {
+  height: 40px;
+  margin: 0;
+  margin-right: 2px;
+  padding: 0 16px;
+  line-height: 38px;
+  background: #fafafa;
+  border: 1px solid #e8e8e8;
+  border-radius: 4px 4px 0 0;
+  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active {
+  height: 40px;
+  color: #1890ff;
+  background: #fff;
+  border-color: #e8e8e8;
+  border-bottom: 1px solid #fff;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active::before {
+  border-top: 2px solid transparent;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-disabled {
+  color: #1890ff;
+  color: rgba(0, 0, 0, 0.25);
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-inactive {
+  padding: 0;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-nav-wrap {
+  margin-bottom: 0;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab .ant-tabs-close-x {
+  width: 16px;
+  height: 14px;
+  margin-right: -5px;
+  margin-left: 3px;
+  overflow: hidden;
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 12px;
+  vertical-align: middle;
+  transition: all 0.3s;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab .ant-tabs-close-x:hover {
+  color: rgba(0, 0, 0, 0.85);
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-content > .ant-tabs-tabpane,
+.ant-tabs.ant-tabs-editable-card .ant-tabs-card-content > .ant-tabs-tabpane {
+  transition: none !important;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-content > .ant-tabs-tabpane-inactive,
+.ant-tabs.ant-tabs-editable-card .ant-tabs-card-content > .ant-tabs-tabpane-inactive {
+  overflow: hidden;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab:hover .anticon-close {
+  opacity: 1;
+}
+.ant-tabs-extra-content {
+  line-height: 45px;
+}
+.ant-tabs-extra-content .ant-tabs-new-tab {
+  position: relative;
+  width: 20px;
+  height: 20px;
+  color: rgba(0, 0, 0, 0.65);
+  font-size: 12px;
+  line-height: 20px;
+  text-align: center;
+  border: 1px solid #e8e8e8;
+  border-radius: 2px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+.ant-tabs-extra-content .ant-tabs-new-tab:hover {
+  color: #1890ff;
+  border-color: #1890ff;
+}
+.ant-tabs-extra-content .ant-tabs-new-tab svg {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  margin: auto;
+}
+.ant-tabs.ant-tabs-large .ant-tabs-extra-content {
+  line-height: 56px;
+}
+.ant-tabs.ant-tabs-small .ant-tabs-extra-content {
+  line-height: 37px;
+}
+.ant-tabs.ant-tabs-card .ant-tabs-extra-content {
+  line-height: 40px;
+}
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-nav-container,
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-nav-container {
+  height: 100%;
+}
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab,
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab {
+  margin-bottom: 8px;
+  border-bottom: 1px solid #e8e8e8;
+}
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab-active,
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active {
+  padding-bottom: 4px;
+}
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab:last-child,
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab:last-child {
+  margin-bottom: 8px;
+}
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-new-tab,
+.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-new-tab {
+  width: 90%;
+}
+.ant-tabs-vertical.ant-tabs-card.ant-tabs-left .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-nav-wrap {
+  margin-right: 0;
+}
+.ant-tabs-vertical.ant-tabs-card.ant-tabs-left .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab {
+  margin-right: 1px;
+  border-right: 0;
+  border-radius: 4px 0 0 4px;
+}
+.ant-tabs-vertical.ant-tabs-card.ant-tabs-left .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab-active {
+  margin-right: -1px;
+  padding-right: 18px;
+}
+.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-nav-wrap {
+  margin-left: 0;
+}
+.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab {
+  margin-left: 1px;
+  border-left: 0;
+  border-radius: 0 4px 4px 0;
+}
+.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active {
+  margin-left: -1px;
+  padding-left: 18px;
+}
+.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab {
+  height: auto;
+  border-top: 0;
+  border-bottom: 1px solid #e8e8e8;
+  border-radius: 0 0 4px 4px;
+}
+.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab-active {
+  padding-top: 1px;
+  padding-bottom: 0;
+  color: #1890ff;
+}
+.ant-tabs {
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0;
+  color: rgba(0, 0, 0, 0.65);
+  font-size: 14px;
+  font-variant: tabular-nums;
+  line-height: 1.5;
+  list-style: none;
+  font-feature-settings: 'tnum', "tnum";
+  position: relative;
+  overflow: hidden;
+  zoom: 1;
+}
+.ant-tabs::before,
+.ant-tabs::after {
+  display: table;
+  content: '';
+}
+.ant-tabs::after {
+  clear: both;
+}
+.ant-tabs-ink-bar {
+  position: absolute;
+  bottom: 1px;
+  left: 0;
+  z-index: 1;
+  box-sizing: border-box;
+  width: 0;
+  height: 2px;
+  background-color: #1890ff;
+  transform-origin: 0 0;
+}
+.ant-tabs-bar {
+  margin: 0 0 16px 0;
+  border-bottom: 1px solid #e8e8e8;
+  outline: none;
+  transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+.ant-tabs-nav-container {
+  position: relative;
+  box-sizing: border-box;
+  margin-bottom: -1px;
+  overflow: hidden;
+  font-size: 14px;
+  line-height: 1.5;
+  white-space: nowrap;
+  transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+  zoom: 1;
+}
+.ant-tabs-nav-container::before,
+.ant-tabs-nav-container::after {
+  display: table;
+  content: '';
+}
+.ant-tabs-nav-container::after {
+  clear: both;
+}
+.ant-tabs-nav-container-scrolling {
+  padding-right: 32px;
+  padding-left: 32px;
+}
+.ant-tabs-bottom .ant-tabs-bottom-bar {
+  margin-top: 16px;
+  margin-bottom: 0;
+  border-top: 1px solid #e8e8e8;
+  border-bottom: none;
+}
+.ant-tabs-bottom .ant-tabs-bottom-bar .ant-tabs-ink-bar {
+  top: 1px;
+  bottom: auto;
+}
+.ant-tabs-bottom .ant-tabs-bottom-bar .ant-tabs-nav-container {
+  margin-top: -1px;
+  margin-bottom: 0;
+}
+.ant-tabs-tab-prev,
+.ant-tabs-tab-next {
+  position: absolute;
+  z-index: 2;
+  width: 0;
+  height: 100%;
+  color: rgba(0, 0, 0, 0.45);
+  text-align: center;
+  background-color: transparent;
+  border: 0;
+  cursor: pointer;
+  opacity: 0;
+  transition: width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  pointer-events: none;
+}
+.ant-tabs-tab-prev.ant-tabs-tab-arrow-show,
+.ant-tabs-tab-next.ant-tabs-tab-arrow-show {
+  width: 32px;
+  height: 100%;
+  opacity: 1;
+  pointer-events: auto;
+}
+.ant-tabs-tab-prev:hover,
+.ant-tabs-tab-next:hover {
+  color: rgba(0, 0, 0, 0.65);
+}
+.ant-tabs-tab-prev-icon,
+.ant-tabs-tab-next-icon {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  font-weight: bold;
+  font-style: normal;
+  font-feature-settings: normal;
+  font-variant: normal;
+  line-height: inherit;
+  text-align: center;
+  text-transform: none;
+  transform: translate(-50%, -50%);
+}
+.ant-tabs-tab-prev-icon-target,
+.ant-tabs-tab-next-icon-target {
+  display: block;
+  display: inline-block;
+  font-size: 12px;
+  font-size: 10px \9;
+  transform: scale(0.83333333) rotate(0deg);
+}
+:root .ant-tabs-tab-prev-icon-target,
+:root .ant-tabs-tab-next-icon-target {
+  font-size: 12px;
+}
+.ant-tabs-tab-btn-disabled {
+  cursor: not-allowed;
+}
+.ant-tabs-tab-btn-disabled,
+.ant-tabs-tab-btn-disabled:hover {
+  color: rgba(0, 0, 0, 0.25);
+}
+.ant-tabs-tab-next {
+  right: 2px;
+}
+.ant-tabs-tab-prev {
+  left: 0;
+}
+:root .ant-tabs-tab-prev {
+  filter: none;
+}
+.ant-tabs-nav-wrap {
+  margin-bottom: -1px;
+  overflow: hidden;
+}
+.ant-tabs-nav-scroll {
+  overflow: hidden;
+  white-space: nowrap;
+}
+.ant-tabs-nav {
+  position: relative;
+  display: inline-block;
+  box-sizing: border-box;
+  margin: 0;
+  padding-left: 0;
+  list-style: none;
+  transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+.ant-tabs-nav::before,
+.ant-tabs-nav::after {
+  display: table;
+  content: ' ';
+}
+.ant-tabs-nav::after {
+  clear: both;
+}
+.ant-tabs-nav .ant-tabs-tab {
+  position: relative;
+  display: inline-block;
+  box-sizing: border-box;
+  height: 100%;
+  margin: 0 32px 0 0;
+  padding: 12px 16px;
+  -webkit-text-decoration: none;
+  text-decoration: none;
+  cursor: pointer;
+  transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+.ant-tabs-nav .ant-tabs-tab::before {
+  position: absolute;
+  top: -1px;
+  left: 0;
+  width: 100%;
+  border-top: 2px solid transparent;
+  border-radius: 4px 4px 0 0;
+  transition: all 0.3s;
+  content: '';
+  pointer-events: none;
+}
+.ant-tabs-nav .ant-tabs-tab:last-child {
+  margin-right: 0;
+}
+.ant-tabs-nav .ant-tabs-tab:hover {
+  color: #40a9ff;
+}
+.ant-tabs-nav .ant-tabs-tab:active {
+  color: #096dd9;
+}
+.ant-tabs-nav .ant-tabs-tab .anticon {
+  margin-right: 8px;
+}
+.ant-tabs-nav .ant-tabs-tab-active {
+  color: #1890ff;
+  text-shadow: 0 0 0.25px currentColor;
+}
+.ant-tabs-nav .ant-tabs-tab-disabled,
+.ant-tabs-nav .ant-tabs-tab-disabled:hover {
+  color: rgba(0, 0, 0, 0.25);
+  cursor: not-allowed;
+}
+.ant-tabs .ant-tabs-large-bar .ant-tabs-nav-container {
+  font-size: 16px;
+}
+.ant-tabs .ant-tabs-large-bar .ant-tabs-tab {
+  padding: 16px;
+}
+.ant-tabs .ant-tabs-small-bar .ant-tabs-nav-container {
+  font-size: 14px;
+}
+.ant-tabs .ant-tabs-small-bar .ant-tabs-tab {
+  padding: 8px 16px;
+}
+.ant-tabs-content::before {
+  display: block;
+  overflow: hidden;
+  content: '';
+}
+.ant-tabs .ant-tabs-top-content,
+.ant-tabs .ant-tabs-bottom-content {
+  width: 100%;
+}
+.ant-tabs .ant-tabs-top-content > .ant-tabs-tabpane,
+.ant-tabs .ant-tabs-bottom-content > .ant-tabs-tabpane {
+  flex-shrink: 0;
+  width: 100%;
+  -webkit-backface-visibility: hidden;
+  opacity: 1;
+  transition: opacity 0.45s;
+}
+.ant-tabs .ant-tabs-top-content > .ant-tabs-tabpane-inactive,
+.ant-tabs .ant-tabs-bottom-content > .ant-tabs-tabpane-inactive {
+  padding: 0 !important;
+}
+.ant-tabs .ant-tabs-top-content > .ant-tabs-tabpane-inactive,
+.ant-tabs .ant-tabs-bottom-content > .ant-tabs-tabpane-inactive {
+  height: 0;
+  overflow: hidden;
+  opacity: 0;
+  pointer-events: none;
+}
+.ant-tabs .ant-tabs-top-content > .ant-tabs-tabpane-inactive input,
+.ant-tabs .ant-tabs-bottom-content > .ant-tabs-tabpane-inactive input {
+  visibility: hidden;
+}
+.ant-tabs .ant-tabs-top-content.ant-tabs-content-animated,
+.ant-tabs .ant-tabs-bottom-content.ant-tabs-content-animated {
+  display: flex;
+  flex-direction: row;
+  transition: margin-left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+  will-change: margin-left;
+}
+.ant-tabs .ant-tabs-left-bar,
+.ant-tabs .ant-tabs-right-bar {
+  height: 100%;
+  border-bottom: 0;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-tab-arrow-show,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-tab-arrow-show {
+  width: 100%;
+  height: 32px;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-tab,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-tab {
+  display: block;
+  float: none;
+  margin: 0 0 16px 0;
+  padding: 8px 24px;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-tab:last-child,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-tab:last-child {
+  margin-bottom: 0;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-extra-content,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-extra-content {
+  text-align: center;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-scroll,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-scroll {
+  width: auto;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container,
+.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-wrap,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-wrap {
+  height: 100%;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container {
+  margin-bottom: 0;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container.ant-tabs-nav-container-scrolling,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container.ant-tabs-nav-container-scrolling {
+  padding: 32px 0;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-wrap,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-wrap {
+  margin-bottom: 0;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-nav,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-nav {
+  width: 100%;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-ink-bar,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-ink-bar {
+  top: 0;
+  bottom: auto;
+  left: auto;
+  width: 2px;
+  height: 0;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-tab-next,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-tab-next {
+  right: 0;
+  bottom: 0;
+  width: 100%;
+  height: 32px;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-tab-prev,
+.ant-tabs .ant-tabs-right-bar .ant-tabs-tab-prev {
+  top: 0;
+  width: 100%;
+  height: 32px;
+}
+.ant-tabs .ant-tabs-left-content,
+.ant-tabs .ant-tabs-right-content {
+  margin-top: 0 !important;
+}
+.ant-tabs .ant-tabs-left-content,
+.ant-tabs .ant-tabs-right-content {
+  width: auto;
+  overflow: hidden;
+}
+.ant-tabs .ant-tabs-left-bar {
+  float: left;
+  margin-right: -1px;
+  margin-bottom: 0;
+  border-right: 1px solid #e8e8e8;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-tab {
+  text-align: right;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container {
+  margin-right: -1px;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-wrap {
+  margin-right: -1px;
+}
+.ant-tabs .ant-tabs-left-bar .ant-tabs-ink-bar {
+  right: 1px;
+}
+.ant-tabs .ant-tabs-left-content {
+  padding-left: 24px;
+  border-left: 1px solid #e8e8e8;
+}
+.ant-tabs .ant-tabs-right-bar {
+  float: right;
+  margin-bottom: 0;
+  margin-left: -1px;
+  border-left: 1px solid #e8e8e8;
+}
+.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container {
+  margin-left: -1px;
+}
+.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-wrap {
+  margin-left: -1px;
+}
+.ant-tabs .ant-tabs-right-bar .ant-tabs-ink-bar {
+  left: 1px;
+}
+.ant-tabs .ant-tabs-right-content {
+  padding-right: 24px;
+  border-right: 1px solid #e8e8e8;
+}
+.ant-tabs-top .ant-tabs-ink-bar-animated,
+.ant-tabs-bottom .ant-tabs-ink-bar-animated {
+  transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), width 0.2s cubic-bezier(0.645, 0.045, 0.355, 1), left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+.ant-tabs-left .ant-tabs-ink-bar-animated,
+.ant-tabs-right .ant-tabs-ink-bar-animated {
+  transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), height 0.2s cubic-bezier(0.645, 0.045, 0.355, 1), top 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+.no-flex > .ant-tabs-content > .ant-tabs-content-animated,
+.ant-tabs-no-animation > .ant-tabs-content > .ant-tabs-content-animated {
+  margin-left: 0 !important;
+  transform: none !important;
+}
+.no-flex > .ant-tabs-content > .ant-tabs-tabpane-inactive,
+.ant-tabs-no-animation > .ant-tabs-content > .ant-tabs-tabpane-inactive {
+  padding: 0 !important;
+}
+.no-flex > .ant-tabs-content > .ant-tabs-tabpane-inactive,
+.ant-tabs-no-animation > .ant-tabs-content > .ant-tabs-tabpane-inactive {
+  height: 0;
+  overflow: hidden;
+  opacity: 0;
+  pointer-events: none;
+}
+.no-flex > .ant-tabs-content > .ant-tabs-tabpane-inactive input,
+.ant-tabs-no-animation > .ant-tabs-content > .ant-tabs-tabpane-inactive input {
+  visibility: hidden;
+}
+.ant-tabs-left-content > .ant-tabs-content-animated,
+.ant-tabs-right-content > .ant-tabs-content-animated {
+  margin-left: 0 !important;
+  transform: none !important;
+}
+.ant-tabs-left-content > .ant-tabs-tabpane-inactive,
+.ant-tabs-right-content > .ant-tabs-tabpane-inactive {
+  padding: 0 !important;
+}
+.ant-tabs-left-content > .ant-tabs-tabpane-inactive,
+.ant-tabs-right-content > .ant-tabs-tabpane-inactive {
+  height: 0;
+  overflow: hidden;
+  opacity: 0;
+  pointer-events: none;
+}
+.ant-tabs-left-content > .ant-tabs-tabpane-inactive input,
+.ant-tabs-right-content > .ant-tabs-tabpane-inactive input {
+  visibility: hidden;
+}

File diff suppressed because it is too large
+ 0 - 0
assets/icons/svg/base-setting.svg


+ 151 - 0
assets/public.css

@@ -243,3 +243,154 @@
   --primary-color: #3f51b5;
 
 }
+
+
+.custom-btn {
+  width: auto;
+  color: #fff;
+  border-radius: 5px;
+  padding: 0 2rem;
+  font-family: 'Lato', sans-serif;
+  font-weight: 500;
+  background: transparent;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  position: relative;
+  display: inline-block;
+  box-shadow: inset 2px 2px 2px 0px rgba(255, 255, 255, .5), 7px 7px 20px 0px rgba(0, 0, 0, .1), 4px 4px 5px 0px rgba(0, 0, 0, .1);
+  outline: none;
+}
+
+/* 3 */
+.btn-3 {
+  background: rgb(0,172,238);
+  background: linear-gradient(0deg, rgba(0,172,238,1) 0%, rgba(2,126,251,1) 100%);
+  width: 130px;
+  height: 40px;
+  line-height: 42px;
+  padding: 0;
+  border: none;
+
+}
+.btn-3 span {
+  position: relative;
+  display: block;
+  width: 100%;
+  height: 100%;
+}
+.btn-3:before,
+.btn-3:after {
+  position: absolute;
+  content: "";
+  right: 0;
+  top: 0;
+  background: rgba(2,126,251,1);
+  transition: all 0.3s ease;
+}
+.btn-3:before {
+  height: 0%;
+  width: 2px;
+}
+.btn-3:after {
+  width: 0%;
+  height: 2px;
+}
+.btn-3:hover{
+  background: transparent;
+  box-shadow: none;
+}
+.btn-3:hover:before {
+  height: 100%;
+}
+.btn-3:hover:after {
+  width: 100%;
+}
+.btn-3 span:hover{
+  color: rgba(2,126,251,1);
+}
+.btn-3 span:before,
+.btn-3 span:after {
+  position: absolute;
+  content: "";
+  left: 0;
+  bottom: 0;
+  background: rgba(2,126,251,1);
+  transition: all 0.3s ease;
+}
+.btn-3 span:before {
+  width: 2px;
+  height: 0%;
+}
+.btn-3 span:after {
+  width: 0%;
+  height: 2px;
+}
+.btn-3 span:hover:before {
+  height: 100%;
+}
+.btn-3 span:hover:after {
+  width: 100%;
+}
+
+/* 13 */
+.btn-13 {
+  background-color: #89d8d3;
+  background-image: linear-gradient(315deg, #89d8d3 0%, #03c8a8 74%);
+  border: none;
+  z-index: 1;
+}
+.btn-13:after {
+  position: absolute;
+  content: "";
+  width: 100%;
+  height: 0;
+  bottom: 0;
+  left: 0;
+  z-index: -1;
+  border-radius: 5px;
+  background-color: #4dccc6;
+  background-image: linear-gradient(315deg, #4dccc6 0%, #96e4df 74%);
+  box-shadow:
+    -7px -7px 20px 0px #fff9,
+    -4px -4px 5px 0px #fff9,
+    7px 7px 20px 0px #0002,
+    4px 4px 5px 0px #0001;
+  transition: all 0.3s ease;
+}
+.btn-13:hover {
+  color: #fff;
+}
+.btn-13:hover:after {
+  top: 0;
+  height: 100%;
+}
+.btn-13:active {
+  top: 2px;
+}
+.manager-con{
+  width: 100%;
+  height: calc(100% - 60px);
+}
+.manager-title{
+  width: calc(100% - 40px);
+  height: 50px;
+  display: flex;
+  align-items: center;
+  border-radius: 6px;
+  background-color: #fff;
+  margin: 10px auto 0;
+  padding: 1rem;
+  box-sizing: border-box;
+  font-size: 1.5rem;
+}
+
+.manager-show{
+  width: calc(100% - 40px);
+  margin: 20px auto;
+  height: calc(100% - 40px);
+  overflow: auto;
+  background-color: #fff;
+  border-radius: 6px;
+  position: relative;
+  position: relative;
+}

+ 1 - 0
assets/publicBanner.css

@@ -7,6 +7,7 @@
   height: 450px;
   justify-content: center;
   align-items: center;
+  margin-bottom: 50px;
 }
 .banner *{
   transition: all .4s;

+ 1 - 1
components/banner/productBanner.vue

@@ -57,5 +57,5 @@ export default {
 </script>
 
 <style scoped>
-@import "~/assets/publicBanner.css";
+@import "@/assets/publicBanner.css";
 </style>

+ 4 - 1
components/header/menuDrops/menuDrop.vue

@@ -72,7 +72,10 @@ export default {
       products(){
         return this.$store.getters.productTypes.filter(item=>item.typeKey!=='all')
       },
-      solutions(){return this.$store.getters.solutionTypes},
+      solutions(){
+        return this.$store.getters.solutionTypes.filter(item=>item.typeKey!=='all')
+      },
+
   },
   // 使用vuex getter 获取数据
   data(){

+ 3 - 3
components/newsTypes.vue

@@ -1,8 +1,8 @@
 <template>
   <div class="content">
-    <big-title>
-      {{lang===langType.cn?"新闻中心":getAbbrText("新闻中心")}}
-    </big-title>
+<!--    <big-title>-->
+<!--      {{lang===langType.cn?"新闻中心":getAbbrText("新闻中心")}}-->
+<!--    </big-title>-->
     <div class="conBox product-type">
       <p
         v-for="(item,i) in types"

+ 348 - 0
components/product/productInfo.vue

@@ -0,0 +1,348 @@
+<script >
+import langMap from "@/map/langMap";
+import {unescapeHtml} from "@/until/unescapeHtml";
+import {timestampToTime} from "@/until/time";
+// 导入ant-design-vue的组件
+import {Tabs, Tag} from 'ant-design-vue'
+// 引入样式, 这里使用单独抽离出来的antd样式
+import '@/assets/antd_tabs.css'
+
+
+import imageViewer from "@/components/public/imageViewer.vue";
+
+/**
+ * emit 事件
+ * tryBuy 用户尝试购买
+ */
+export default {
+  name: "productInfo",
+  components: {
+    ATabs: Tabs,
+    ATabPane: Tabs.TabPane,
+    ATag: Tag,
+    imageViewer
+  },
+  props: {
+    productId: {
+      type: Number,
+    },
+    lang: {
+      type: String,
+      default: langMap.lang.zh
+    },
+    unescape: {
+      type: Boolean,
+      default: true
+    },
+    product: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      showImage: '',
+      productInfo: {
+      }
+    }
+  },
+  watch: {
+    // 监听product变化
+    product(newVal, oldVal) {
+      this.productInfoChange();
+    }
+  },
+  beforeMount() {
+    this.productInfoChange();
+  },
+  mounted() {
+    this.$forceUpdate()
+  },
+  methods: {
+    productInfoChange(){
+      let data = this.product;
+      if(this.unescape){
+        data.detail = unescapeHtml(data.detail);
+        data.overview = unescapeHtml(data.overview);
+        data.paramter = unescapeHtml(data.paramter);
+      }
+
+      data.add_time = timestampToTime(data.add_time);
+      console.log(data)
+      this.$nextTick(()=>{
+        this.showImage = data.image;
+        //this.$forceUpdate()
+      })
+      // 将产品主图添加至第一个子图中
+      if( data.sub_images){
+        console.log(data.image)
+        data.sub_images.unshift(data.image);
+      }
+      this.productInfo = data;
+    },
+    parseTags(tagArrStr){
+      console.log(tagArrStr)
+      let tags = tagArrStr
+      if (!tags) {
+        return [];
+      }
+      // 去除空字符串
+      tags = tags.replace(/\s/g,'')
+      let tagArr = tags.split(',')
+      // 标签去重
+      tagArr = [...new Set(tagArr)]
+      // 移除空标签
+      tagArr = tagArr.filter(item => item)
+      return tagArr
+    },
+    getLangText(str) {
+      return langMap.getText(this.lang, str);
+    },
+    /**
+     * 图片路径处理, 兼容手动上传的图片与后期自动增加的图片
+     * @param text
+     * @return {*|string|string}
+     */
+    imagePathBabel(text){
+      let result = text?text.charAt(0) == '/'? text : '/public/'+text : ''
+      return result
+    },
+    changeShowImages(nextIndex){
+      let ul = this.$refs.imageList;
+      let ulWidth = ul.offsetWidth;
+      let parentWidth = ul.parentElement.offsetWidth;
+      let nextImage = this.productInfo.sub_images[nextIndex]
+      // 计算 top偏移值
+      let left = 0;
+      left = nextIndex * 100 + (10 * nextIndex);
+      // 偏移位置锁定
+      if (ulWidth - parentWidth < left) {
+        left = ulWidth - parentWidth;
+      }
+      if (left < 0) {
+        left = 0;
+      }
+      // 移动图片组件
+      ul.style.left = `-${left}px`
+
+      this.showImage = nextImage
+    },
+    tryBuy(){
+      this.$emit('tryBuy')
+    }
+  }
+}
+</script>
+
+<template>
+  <div class="product_view">
+    <div class="grid product">
+      <div class="product_image">
+        <div class="product-image">
+          <img class="active"
+               :src="imagePathBabel(showImage)" ></img>
+        </div>
+        <div class="sub_images">
+          <div class="con-btn prev">
+            <svg-icon icon-class="prev" />
+          </div>
+          <ul class="image-list" ref="imageList" >
+            <li class="image-item"
+                v-for="(subImage, index) in productInfo.sub_images"
+                :key="`${productInfo.proid}-sub-${index}-${subImage}`"
+                @click="changeShowImages(index)"
+            >
+              <img :src="imagePathBabel(subImage)">
+            </li>
+          </ul>
+          <div class="con-btn next">
+            <svg-icon icon-class="next" />
+          </div>
+        </div>
+      </div>
+      <div class="product_brief">
+        <h1>{{ productInfo.name }}</h1>
+        <div class="tags">
+          <a-tag
+            v-for="tag in parseTags(productInfo.seo_key)"
+            :key="tag"
+            color="#2db7f5"
+          >
+            {{ tag }}
+          </a-tag>
+        </div>
+        <div class="description" v-html="productInfo.overview">
+        </div>
+        <button class="add-to-cart" @click="tryBuy">{{ getLangText("联系购买") }}</button>
+      </div>
+    </div>
+    <div class="product_detail">
+      <!--    产品介绍-->
+      <!--    产品详情-->
+      <a-tabs default-active-key="detail" >
+        <a-tab-pane key="detail" :tab="getLangText(`介绍`)">
+          <div class="description" v-html="productInfo.detail">
+          </div>
+        </a-tab-pane>
+        <a-tab-pane key="parameter" :tab="getLangText(`参数`)">
+          <div class="description" v-html="productInfo.parameter">
+          </div>
+        </a-tab-pane>
+      </a-tabs>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+
+h1{
+  font-size: 2.5rem;
+}
+.product_view{
+  width: 100%;
+  height: 100%;
+  padding: 0.5rem;
+  position: relative;
+}
+.product{
+  width: 100%;
+  height: auto;
+  display: flex;
+  flex-wrap: wrap;
+}
+.product .product_image{
+  width: 40%;
+  height: auto;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 1rem;
+  box-sizing: border-box;
+}
+.product .product_brief{
+  width: calc(60% - 10px);
+  height: auto;
+  min-height: 510px;
+  padding: 5px 10px;
+  box-sizing: border-box;
+  border-left: 1px solid #e3dddd;
+}
+.product_image .product-image{
+  width: calc(100% - 5px);
+  min-width: 200px;
+  max-width: 400px;
+  height: auto;
+}
+.product_image .product-image img{
+  animation: fadeImg 1s;
+  width: 100%;
+  height: auto;
+}
+.product_image .sub_images{
+  width: 100%;
+  height: 110px;
+  padding: 5px 0;
+  box-sizing: border-box;
+  display: flex;
+  position: relative;
+  overflow: hidden;
+}
+.product_image .sub_images .con-btn{
+  width: 30px;
+  height: calc(100% - 10px);
+  position: absolute;
+  top: 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+}
+.product_image .sub_images .con-btn:hover{
+  background-color: rgba(0,0,0,0.1);
+}
+.sub_images .prev{
+  left: 0;
+}
+.sub_images .next{
+  right: 0;
+}
+.sub_images .image-list{
+  width: auto;
+  height: 100px;
+  display: flex;
+  position: absolute;
+  top: 0;
+}
+.sub_images .image-list .image-item{
+  width: 100px;
+  margin-right: 10px;
+  height: 100%;
+}
+.sub_images .image-list .image-item img{
+  object-fit: cover;
+  width: 100%;
+  height: 100%;
+}
+
+.tags{
+  width: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+  align-items: center;
+  padding: 5px;
+  box-sizing: border-box;
+}
+
+
+
+.description {
+  padding: 1rem;
+}
+
+.add-to-cart{
+  position: relative;
+  display: inline-block;
+  background: #3e3e3f;
+  color: #fff;
+  border: none;
+  border-radius: 0;
+  padding: 1rem 2.5rem;
+  font-size: 1rem;
+  text-transform: uppercase;
+  cursor: pointer;
+  transform: translateZ(0);
+  transition: color 0.3s ease;
+  letter-spacing: 0.0625rem;
+}
+.add-to-cart:hover::before {
+  transform: scaleX(1);
+}
+.add-to-cart::before {
+  position: absolute;
+  content: "";
+  z-index: -1;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: #565657;
+  transform: scaleX(0);
+  transform-origin: 0 50%;
+  transition: transform 0.3s ease-out;
+}
+@keyframes fadeImg {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+
+</style>

+ 12 - 4
components/public/form/inputRow.vue

@@ -6,6 +6,10 @@ export default {
       type: String,
       default: ''
     },
+    labelWidth: {
+      type: Number,
+      default: 0
+    },
     form: {
       type: Object,
       default: () => {}
@@ -38,8 +42,10 @@ export default {
 
 <template>
   <div class="w-full px-1.5 flex">
-    <div class="w-4/12 flex items-center u px-1">
-      <p class="w-full text-justify">
+    <div class="w-4/12 flex items-center u px-1 justify-center"
+         :style="`${labelWidth?`width:${labelWidth}px;`:''}`"
+    >
+      <span class="w-full flex items-center justify-center">
         {{ label }}
 <!--        显示一个问号,悬浮时显示remark, 不使用 a toolTip -->
         <a-tooltip v-if="remark" placement="topLeft">
@@ -48,9 +54,11 @@ export default {
           </template>
           <a-icon type="question-circle" />
         </a-tooltip>
-      </p>
+      </span>
     </div>
-    <div class="w-8/12">
+    <div class="w-8/12"
+         :style="`${labelWidth?`width:calc(100% - ${labelWidth}px);`:''}`"
+    >
       <slot></slot>
       <div :class="`w-full  ${state===0?'text-red-600':'text-green-300'}`" v-show="msg" >
         {{msg}}

+ 5 - 1
components/public/imageViewer.vue

@@ -5,6 +5,10 @@ export default {
     src: {
       type: String,
       default: ''
+    },
+    alt: {
+      type: String,
+      default: ''
     }
   },
   data(){
@@ -18,7 +22,7 @@ export default {
 
 <template>
 <div class="img">
-  <img :src="src" />
+  <img :src="src" :alt="alt" />
   <div v-show="isFull" class="img-con">
     <div class="img-btn">+</div>
     <div class="img-btn">-</div>

+ 16 - 5
components/productTypes.vue → components/showTypes.vue

@@ -1,9 +1,8 @@
 <template>
   <div class="w-screen pad:w-full">
-    <big-title>{{lang===langType.cn?"产品类别":getAbbrText("产品类别")}}</big-title>
     <div class="conBox container product-type">
       <p
-        v-for="(item,i) in types"
+        v-for="(item,i) in showTypes"
         :key="item.key"
         :class="`type-item ${item.key===type?'type-selected':''}`"
         @click="selectType(item.key)"
@@ -23,7 +22,7 @@ import {pTypes} from "~/map/productMap";
 import BigTitle from "~/components/public/bigTitle.vue";
 
 export default {
-  name: "productTypes",
+  name: "showTypes",
   components: {BigTitle},
   props: {
     lang:{
@@ -37,7 +36,11 @@ export default {
       required: true
     },
     title: {
-      default: '产品类别'
+      default: '类别'
+    },
+    maxShow: {
+      type: Number,
+      default: 6,
     }
   },
   data(){
@@ -45,6 +48,14 @@ export default {
       langType: langMap.lang,
     }
   },
+  computed: {
+    showTypes(){
+      // 移除最后几个元素
+      let types = this.types.slice(0, this.maxShow);
+      return types;
+    }
+  },
+
   methods:{
     getLangText(str) {
       return langMap.getText(this.lang, str);
@@ -98,7 +109,7 @@ export default {
   padding-bottom: 20px;
 }
 .product-type .type-selected .icon-box{
-  border-bottom: 1px solid deepskyblue;
+  border-bottom: 2px solid deepskyblue;
 }
 .product-type .type-item .type-name{
   width: 100%;

+ 1 - 1
components/solutionTypes.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="content">
-    <big-title>{{lang===langType.cn?"解决方案":getAbbrText("解决方案")}}</big-title>
+<!--    <big-title>{{lang===langType.cn?"解决方案":getAbbrText("解决方案")}}</big-title>-->
     <div class="conBox product-type">
       <p
         v-for="(item,i) in types"

+ 11 - 0
map/adminSideBar.js

@@ -25,6 +25,17 @@ export const adminMenus = [
       },
     ]
   },
+  {
+    title: '基础信息配置',
+    icon: 'base-setting',
+    child: [
+      {
+        key: 'base',
+        title: '基础信息管理',
+        path: '/manger/base'
+      },
+    ]
+  },
   {
     title: '产品管理',
     icon: 'product-setting',

+ 15 - 0
map/apiMap.js

@@ -22,6 +22,15 @@ export const apiMap = {
   productInfo: {
     path: `/api/product`
   },
+  productEdit: {
+    path: `/api/product/edit`
+  },
+  productDelete: {
+    path: `/api/product/del`
+  },
+  productAdd: {
+    path: `/api/product/add`
+  },
   productTypes: {
     path: `/api/product/types`
   },
@@ -95,6 +104,12 @@ export const apiMap = {
   },
   baseUpdateCarousel: {
     path: `/api/base/carousel`
+  },
+  baseInfo: {
+    path: `/api/base/info`
+  },
+  baseInfoEdit: {
+    path: `/api/base/edit`
   }
 }
 

+ 12 - 0
map/langMap.js

@@ -96,6 +96,18 @@ const textArr = [
     keys: ["新闻中心"],
     "en-us": 'News',
   },
+  {
+    keys: ["联系购买"],
+    "en-us": 'Contact purchase',
+  },
+  {
+    keys: ["介绍"],
+    "en-us": 'Introduction'
+  },
+  {
+    keys: ["参数"],
+    "en-us": 'Parameters'
+  },
 ]
 const i18Paths =
   [

+ 93 - 0
pages/manger/base.vue

@@ -0,0 +1,93 @@
+<script>
+
+
+import AdminLayout from "~/components/layout/adminLayout.vue";
+import {adminMenus} from "../../map/adminSideBar";
+import Vue from 'vue'
+import antd from 'ant-design-vue'
+import 'ant-design-vue/dist/antd.css'
+import {login_types} from "../../store/login";
+import {handle} from "../../until/handle";
+Vue.use(antd);
+
+export default {
+  name: "mangerBase",
+  components: {AdminLayout},
+  async asyncData(ctx) {
+    // 判断 store 中的 isLogin 是否为 true $store.state.login.captcha
+    console.log(ctx?.store?.state?.login)
+    if (!ctx?.store?.state?.login?.isLogin) {
+      // 如果未登录,重定向到登录页
+      // ctx.redirect('/manger/login')
+      ctx.redirect('/manger/login')
+    }
+    return {}
+  },
+  data(){
+    return {
+      headerMenus:[
+        {
+          text: '前往首页',
+          href: '/',
+          key: 'toIndex'
+        }
+      ],
+      sidebarMenus: adminMenus
+
+    }
+  },
+  computed: {
+    userInfo(){
+      return this.$store.state.login.userInfo
+    }
+  },
+  methods: {
+    dataLogOut(){
+      this.$store.commit('login/'+login_types.mutations.userLogout);
+    },
+    async logout(){
+      let [err,res] = await handle(this.$axios.get('/api/user/logout'));
+      this.dataLogOut();
+      if(err){
+        return {};
+      }
+      this.$message.success('退出成功');
+      // 切换url至 /login
+      this.$router.push('/manger/login');
+    }
+  }
+}
+</script>
+
+<template>
+  <div class="w-full">
+    <admin-layout
+      :logoTitle="'深圳合方圆站点管理'"
+      :header-menus="headerMenus"
+      :sidebar-menus="sidebarMenus"
+      index-path="/manger"
+    >
+      <!--  下拉菜单,对应slot user-->
+      <a-dropdown slot="user">
+        <a class="ant-dropdown-link" href="#">
+          {{ userInfo.name }} <a-icon type="down" />
+        </a>
+        <a-menu slot="overlay">
+          <!--      <a-menu-info key="1">-->
+          <!--        <a href="#">1st menu info</a>-->
+          <!--      </a-menu-info>-->
+          <a-menu-item key="3">
+            <span @click="logout">退出登录</span>
+          </a-menu-item>
+        </a-menu>
+      </a-dropdown>
+      <nuxt-child>
+
+      </nuxt-child>
+    </admin-layout>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 269 - 0
pages/manger/base/index.vue

@@ -0,0 +1,269 @@
+<script>
+
+import RoundedTitle from "@/components/public/roundedTitle.vue";
+import BigTitle from "@/components/public/bigTitle.vue";
+import InputRow from "@/components/public/form/inputRow.vue";
+import ImageViewer from "@/components/public/imageViewer.vue";
+import {FormVerify} from "kind-form-verify";
+import {fieldCheck} from "@/until/form/fieldVerify";
+import {isEmpty} from "@/until/typeTool";
+import {initForm} from "@/until/formTool";
+import ImageTable from "@/components/public/imageTable.vue";
+import Pop from "@/components/public/pop.vue";
+import PopCard from "@/components/public/popCard.vue";
+import {apiMap} from "@/map/apiMap";
+import {handle} from "@/until/handle";
+import {rCode} from "@/map/rcodeMap_esm";
+
+
+let formVerify = null;
+export default {
+  name: 'baseSetting',
+  components: {
+    PopCard, Pop, ImageTable,
+    ImageViewer,
+    InputRow,
+    RoundedTitle,
+    BigTitle
+  },
+  data() {
+    return {
+      editId: 0,
+      form: {
+        wx_qrc: {
+          val: '',
+          init: '',
+          msg: '',
+          state: 0,
+          reCheckField: 'fileData',
+        },
+        addr: {
+          val: '',
+          init: '',
+          msg: '',
+          state: 0,
+        },
+        tel: {
+          val: '',
+          init: '',
+          msg: '',
+          state: 0,
+        },
+        fax: {
+          val: '',
+          init: '',
+          msg: '',
+          state: 0,
+        },
+        email: {
+          val: '',
+          init: '',
+          msg: '',
+          state: 0,
+        },
+        shop_addr: {
+          val: '',
+          init: '',
+          msg: '',
+          state: 0,
+          reCheckField: 'url'
+        }
+      },
+      imagePopShow: false,
+      imagePopLoading: false,
+      imageSelect: {
+        type: '',
+        param: ''
+      },
+
+    }
+  },
+  beforeMount() {
+    formVerify = new FormVerify(
+      this.form,
+      fieldCheck,
+    )
+    formVerify.onLog = (msg) => {
+      console.log(msg);
+    };
+    this.fetchData()
+  },
+  beforeDestroy() {
+    formVerify = null;
+  },
+  methods: {
+    initForm(){
+      initForm(this.form);
+      this.form.wx_qrc.showText = this.form.wx_qrc.val;
+      this.$forceUpdate()
+    },
+    /**
+     * 图片路径处理, 兼容手动上传的图片与后期自动增加的图片
+     * @param text
+     * @return {*|string|string}
+     */
+    imagePathBabel(text){
+      return text?text.charAt(0) == '/'? text : '/public/'+text : ''
+    },
+    openImageSelect(type, param){
+      this.imageSelect = {type, param}
+      this.imagePopShow = true;
+    },
+    selectImageHandle(fileData){
+      this.form.wx_qrc.val = fileData.filePath
+      this.form.wx_qrc.msg = '';
+      this.form.wx_qrc.state = 0;
+      this.form.wx_qrc.showText = fileData.filePath
+    },
+    async onSubmitHandle(){
+      if(!formVerify.checkForm(this.form, true)){
+        return this.$message.error('表单校验失败')
+      }
+      let formData = formVerify.getFormData();
+      formData.id = this.editId;
+      let url = apiMap.baseInfoEdit.path;
+      let actionText = '保存';
+      let [err,res] = await handle(this.$axios.post(
+        url,
+        formData
+      ));
+      if(err){
+        console.log(err);
+        return this.$message.error(`${actionText}失败`);
+      }
+      let  result = res.data;
+      if (result.code === rCode.OK){
+        this.$message.success(`${actionText}成功`);
+      }else{
+        this.$message.error(`${actionText}失败,${result.msg}`);
+      }
+    },
+    async fetchData(){
+      let url = apiMap.baseInfo.path;
+      this.loading = true;
+      let [err, res] = await handle(this.$axios.get(url));
+      this.loading = false;
+      if(err){
+        this.$message.error('获取基础信息失败');
+        return console.log(err);
+      }
+      let result = res.data;
+      if (result.code !== rCode.OK) {
+        this.$message.error(`获取产品信息失败,${result.msg}`)
+        return
+      }
+      console.log(result)
+      let data = result.data;
+      this.editId = data.id;
+      this.form.wx_qrc.init = data.wx_qrc;
+      this.form.addr.init = data.addr;
+      this.form.tel.init = data.tel;
+      this.form.fax.init = data.fax;
+      this.form.email.init = data.email;
+      this.form.shop_addr.init = data.shop_addr;
+      this.initForm()
+
+    }
+  }
+}
+</script>
+
+<template>
+<div class="manager-con">
+
+  <div class="manager-title">
+    <div class="big-title justify-start py-2">
+      <span>
+        基础信息配置
+      </span>
+      <div class="hr"></div>
+    </div>
+  </div>
+
+  <div class="manager-show">
+
+    <input-row class="mt-3"
+               :msg="form.tel.msg"
+               :label-width="120"
+               label="公司电话">
+      <a-input class="!w-48 "
+               v-model="form.tel.val"
+               placeholder="请输入公司电话"></a-input>
+    </input-row>
+    <input-row class="mt-3"
+               :msg="form.fax.msg"
+               :label-width="120"
+               label="公司传真">
+      <a-input class="!w-48 "
+               v-model="form.fax.val"
+               placeholder="请输入公司传真"></a-input>
+    </input-row>
+    <input-row class="mt-3"
+               :msg="form.email.msg"
+               :label-width="120"
+               label="公司邮箱">
+      <a-input class="!w-48 "
+               v-model="form.email.val"
+               placeholder="请输入公司邮箱"></a-input>
+    </input-row>
+    <input-row class="mt-3"
+               :msg="form.addr.msg"
+               :label-width="120"
+               label="公司地址">
+      <a-input class="!w-1/2 "
+               v-model="form.addr.val"
+               placeholder="请输入公司地址"></a-input>
+    </input-row>
+    <input-row class="mt-3"
+               :msg="form.shop_addr.msg"
+               :label-width="120"
+               label="网店地址">
+      <a-input class="!w-1/2 "
+               v-model="form.shop_addr.val"
+               placeholder="网店的访问链接"></a-input>
+    </input-row>
+    <input-row class="mt-3"
+               :msg="form.wx_qrc.msg"
+               :label-width="120"
+               label="微信二维码"
+               remark="在网站浏览时显示联络用的二维码"
+    >
+      <div class="rounded relative" style="width: 524px;height: 360px" >
+        <image-viewer class="" :src="imagePathBabel(form.wx_qrc.showText)"
+                      :alt="form.wx_qrc.showText" ></image-viewer>
+        <div class="absolute w-full h-full left-0 top-0
+                justify-center text-white bg-gray-400
+                items-center text-2xl flex opacity-0 hover:opacity-70"
+             @click="openImageSelect()"
+        >
+          点击选择产品图片
+        </div>
+      </div>
+    </input-row>
+
+    <div class="mt-2">
+      <a-button type="primary" @click="onSubmitHandle">
+        保存
+      </a-button>
+    </div>
+  </div>
+
+  <!--  图片选择弹窗 -->
+  <pop :show="imagePopShow" :loading="imagePopLoading">
+    <pop-card>
+      <template slot="header">
+        <span>选择需要插入的图片</span>
+      </template>
+      <image-table
+        class="w-full h-full"
+        @cancel="imagePopShow = false"
+        @ok="selectImageHandle">
+      </image-table>
+    </pop-card>
+  </pop>
+</div>
+</template>
+
+<style scoped>
+
+</style>

+ 19 - 0
pages/manger/index/showing.vue

@@ -0,0 +1,19 @@
+<script>
+  export default {
+    name: "showing",
+    data() {
+      return {
+      }
+    }
+  }
+</script>
+
+<template>
+  <div class="w-full p-2">
+    该板块正在调整中
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 4 - 3
pages/manger/news/add.vue

@@ -191,7 +191,6 @@ export default {
     editModel(){
       this.isEdit = true;
 
-
       this.editId = this.article.id;
       // 查找对应的 类型
       this.form.title.init = this.article.title;
@@ -210,7 +209,6 @@ export default {
         this.form.pType.init = type.parent_type;
         this.form.pType.val = type.parent_type;
       }
-
     },
     openCoverSelect(){
       console.log('打开文章封面选择窗口');
@@ -352,7 +350,10 @@ export default {
       </div>
     </input-row>
 
-    <ckeditor class="mt-2" ref="ckeditor" :editor="editor" v-model="editorData" :config="editorConfig"></ckeditor>
+    <ckeditor class="mt-2" ref="ckeditor"
+              :editor="editor"
+              v-model="editorData"
+              :config="editorConfig"></ckeditor>
 
     <div class="mt-2">
       <a-button type="primary" @click="onSubmitHandle">

+ 17 - 6
pages/manger/news/info.vue

@@ -15,7 +15,8 @@ export default {
     return {
       loading: false,
       newsInfo: {},
-
+      newsId: '',
+      editUrl_: ''
     }
   },
   beforeMount() {
@@ -26,9 +27,13 @@ export default {
       window.location.href = "/manger/news";
       return ;
     }
-    console.log(this.newsId);
     this.loadNewsInfo();
-
+  },
+  computed:{
+    editUrl: function(){
+      let id = this.newsInfo.id;
+      return `/manger/news/edit?id=${id}`
+    }
   },
   methods: {
     async loadNewsInfo(){
@@ -64,10 +69,16 @@ export default {
 <template>
   <div class="w-full p-2">
     <rounded-title class="text-xl">
-      <span>文章详情</span>
-      <a href="/manger/news"
-         class="px-10 h-full ml-5 rounded bg-blue-400 text-white cursor-pointer
+      <div>
+        <span>文章详情</span>
+        <a href="/manger/news"
+           class="px-10 h-full ml-5 rounded bg-blue-400 text-white cursor-pointer
         hover:text-orange-500 ">文章中心</a>
+      </div>
+
+      <a
+        :href="editUrl"
+        class="edit-btn custom-btn btn-13">编辑</a>
     </rounded-title>
     <div class="page-content-box w-full mt-2 p-0.5  rounded bg-white">
       <div class="page-title">{{newsInfo.title}}</div>

+ 1 - 1
pages/manger/news/type.vue

@@ -81,7 +81,7 @@ export default {
     }
   },
   methods: {
-    // 获取产品类型列表
+    // 获取文章类型列表
     async getTypes() {
       this.loading = true;
       let [err, res] = await handle(axios.get(apiMap.newsTypes.path));

+ 634 - 0
pages/manger/product/add.vue

@@ -0,0 +1,634 @@
+<script >
+
+import RoundedTitle from "@/components/public/roundedTitle.vue";
+import TableSelect from "@/components/public/tableSelect.vue";
+import InputRow from "@/components/public/form/inputRow.vue";
+import ImageViewer from "@/components/public/imageViewer.vue";
+import Pop from "@/components/public/pop.vue";
+import PopCard from "@/components/public/popCard.vue";
+import ImageTable from "@/components/public/imageTable.vue";
+import {escapeHtml, unescapeHtml} from "@/until/unescapeHtml";
+import langMap from "@/map/langMap";
+import {initForm} from "@/until/formTool";
+import {FormVerify} from "kind-form-verify";
+import {fieldCheck} from "@/until/form/fieldVerify";
+import {isEmpty} from "@/until/typeTool";
+import {apiMap} from "@/map/apiMap";
+import {handle} from "@/until/handle";
+import {rCode} from "@/map/rcodeMap_esm";
+
+let ClassicEditor;
+if (process.client) {
+  ClassicEditor = require('../../../model/ckeditor/ckeditor');
+}
+let formVerify = null;
+let baseEditConfig = {
+  language: 'zh',
+  customText: "插入图片",
+}
+const imageSelectType = {
+  main: "main",
+  sub: "sub",
+  editor: "editor",
+}
+export default {
+  name: "productAdd",
+  components: {ImageTable, PopCard, Pop, ImageViewer, InputRow, TableSelect, RoundedTitle},
+  props: ['product'],
+  data() {
+    return {
+      isEdit: false,
+      editId: null,
+      overviewEditorConfig: { ...baseEditConfig },
+      detailEditorConfig: { ...baseEditConfig },
+      parameterEditorConfig: { ...baseEditConfig },
+      overviewEditor: ClassicEditor,
+      detailEditor: ClassicEditor,
+      parameterEditor: ClassicEditor,
+      overviewEditorData: '<p>这是产品的简介</p>',
+      detailEditorData: '<p>这是产品的详细介绍</p>',
+      parameterEditorData: '<p>这是产品的参数介绍, 推荐使用表格详细介绍</p>',
+      langMap: langMap.lang,
+      form: {
+        type: {
+          val: 'all',
+          init: '',
+          msg: '',
+          state: 0,
+          options: this.$store.getters.productTypes,
+          disables: ['all'],
+        },
+        name: {
+          val: '',
+          init: '',
+          msg: '',
+          state: 0
+        },
+        remark: {
+          val: '',
+          init: '',
+          msg: '',
+          state: 0
+        },
+        sort: {
+          val: 0,
+          init: 0,
+          msg: '',
+          state: 0
+        },
+        image: {
+          val: '',
+          init: '',
+          msg: '',
+          reCheckField: 'fileData',
+          showText: '',
+          state: 0
+        },
+        seo_key: {
+          val: "",
+          oldVal: "",
+          init: "",
+          msg: "",
+          state: 0,
+        }
+      },
+      sub_images: [],
+      imageSelectType: imageSelectType,
+      // 图片选择的参数
+      imagePopShow: false,
+      imagePopLoading: false,
+      imageSelect: {
+        type: '',
+        param: ''
+      },
+      previewShow: false,
+      previewLoading: false,
+      previewData: {
+      },
+    }
+  },
+  watch: {
+    product(val){
+      // 更新文章数据
+      this.editModel()
+    }
+  },
+  computed: {
+    tags () {
+      // 拆分tags
+      let tags = this.form.seo_key.val
+      if (!tags) {
+        return [];
+      }
+      // 去除空字符串
+      tags = tags.replace(/\s/g,'')
+      let tagArr = tags.split(',')
+      // 标签去重
+      tagArr = [...new Set(tagArr)]
+      // 移除空标签
+      tagArr = tagArr.filter(item => item)
+      return tagArr
+    }
+  },
+  beforeMount() {
+    this.detailEditorConfig.customFunction = () => {
+      this.openImageSelect(imageSelectType.editor, 'detail');
+    };
+    this.overviewEditorConfig.customFunction = () => {
+      this.openImageSelect(imageSelectType.editor, 'overview');
+    };
+    this.parameterEditorConfig.customFunction = () => {
+      this.openImageSelect(imageSelectType.editor, 'parameter');
+    };
+  },
+  mounted() {
+    formVerify = new FormVerify(
+      this.form,
+      fieldCheck,
+    )
+    // fieldCheck.checkField('type',this.form.type.val)
+    // formVerify.checkForm(this.form, true);
+    console.log(formVerify);
+    formVerify.onLog = (msg) => {
+      console.log(msg);
+    };
+
+
+    // 判断是否为编辑文章
+    if(!isEmpty(this.product)){
+      this.isEdit = true;
+    }
+    this.initForm();
+
+  },
+  beforeDestroy() {
+    formVerify = null;
+  },
+  methods: {
+    initForm(){
+      initForm(this.form);
+      this.form.image.showText = this.form.image.val;
+      // 强制更新页面
+      this.$forceUpdate()
+    },
+    /**
+     * 图片路径处理, 兼容手动上传的图片与后期自动增加的图片
+     * @param text
+     * @return {*|string|string}
+     */
+    imagePathBabel(text){
+      return text?text.charAt(0) == '/'? text : '/public/'+text : ''
+    },
+    openImageSelect(type, param){
+      console.log('打开文章封面选择窗口');
+      this.imageSelect = {type, param}
+      this.imagePopShow = true;
+    },
+    deleteTag(tag){
+      let tagArr = this.tags
+      let index = tagArr.indexOf(tag)
+      tagArr.splice(index, 1)
+      this.form.seo_key.val = tagArr.join(',')
+      this.form.seo_key.msg = ''
+    },
+    // 子图转换
+    translateSubImage(arr){
+      let sub_images = arr.map((item)=>{
+        return {
+          id: item.id,
+          isAdd: false,
+          path: item.path
+        }
+      })
+      return sub_images
+    },
+    addSubImage(){
+      this.sub_images.push('')
+    },
+    deleteSubImage(i){
+      this.sub_images.splice(i, 1)
+    },
+    editorAddImage(editor, fileData){
+      switch (editor) {
+        case 'overview':
+          this.overviewEditorData += `<img src="${fileData.filePath}" alt="${fileData.fileName}">`;
+          break;
+        case 'detail':
+          this.detailEditorData += `<img src="${fileData.filePath}" alt="${fileData.fileName}">`;
+          break;
+        case 'parameter':
+          this.parameterEditorData += `<img src="${fileData.filePath}" alt="${fileData.fileName}">`;
+          break;
+      }
+    },
+    selectImageHandle(fileData){
+      console.log('选择图片')
+      console.log(fileData)
+      this.imagePopShow = false;
+      switch (this.imageSelect.type) {
+        case imageSelectType.main:
+          console.log('选择产品主图');
+          this.form.image.val = fileData.filePath
+          this.form.image.msg = '';
+          this.form.image.state = 0;
+          this.form.image.showText = fileData.filePath
+          console.log(fileData.path)
+          console.log(this.form.image)
+          break;
+        case imageSelectType.sub:
+          console.log(`选择产品子图 param:${this.imageSelect.param}`);
+          this.sub_images[this.imageSelect.param] = fileData.filePath
+          break;
+        case imageSelectType.editor:
+           this.editorAddImage(this.imageSelect.param, fileData)
+           break;
+        default:
+          console.log('未知类型选择图片');
+          break;
+      }
+
+    },
+    editModel(){
+      console.log(`editModel`)
+      this.isEdit = true;
+      this.editId = this.product.proid;
+      this.form.type.init = this.product.type_key;
+      this.form.name.init = this.product.name;
+      this.form.sort.init = this.product.sort;
+      this.form.remark.init = this.product.remark;
+      this.form.image.init = this.product.image;
+      this.form.seo_key.init = this.product.seo_key;
+      this.sub_images = this.product.sub_images;
+      this.overviewEditorData = unescapeHtml(this.product.overview) || "";
+      this.detailEditorData = unescapeHtml(this.product.detail)  || "";
+      this.parameterEditorData = unescapeHtml(this.product.parameter)  || "";
+      this.initForm()
+    },
+    async onSubmitHandle(){
+      console.log('提交产品');
+      let isPass = formVerify.checkForm(this.form, true);
+      if(!isPass){
+        console.log(this.form);
+        this.$message.error('数据验证不通过');
+        return console.log('数据验证不通过');
+      }
+      let formData = formVerify.getFormData();
+      formData.detail = escapeHtml(this.detailEditorData);
+      formData.overview = escapeHtml(this.overviewEditorData);
+      formData.parameter = escapeHtml(this.parameterEditorData);
+      formData.sub_images = this.sub_images;
+      let queryPath = this.isEdit? apiMap.productEdit : apiMap.productAdd;
+      let actionText = this.isEdit? '修改产品': '新增产品';
+      if(this.isEdit){
+        formData.proid = this.editId;
+
+      }
+      let [err,res] = await handle(this.$axios.post(
+        queryPath.path,
+        formData
+      ));
+      if(err){
+        console.log(err);
+        return this.$message.error(`${actionText}失败`);
+      }
+      let  result = res.data;
+      if (result.code === rCode.OK){
+        this.$message.success(`${actionText}成功`);
+      }else{
+        this.$message.error(`${actionText}失败,${result.msg}`);
+      }
+
+    },
+    preview(){
+      console.log('预览产品');
+      this.previewShow = true;
+      this.previewLoading = true;
+
+      setTimeout(()=>{
+        this.$nextTick(()=>{
+          this.previewLoading = false;
+          this.previewData = this.getPreviewData();
+          // 强制刷新
+          this.$forceUpdate()
+        })
+      }, 500)
+    },
+    cancelPreview(){
+      console.log('关闭预览')
+      this.previewLoading = false;
+      this.previewShow = false;
+    },
+    getPreviewData(){
+      let data = {
+        proid: this.editId,
+        type_key: this.form.type.val,
+        name: this.form.name.val,
+        sort: this.form.sort.val,
+        remark: this.form.remark.val,
+        image: this.form.image.val,
+        seo_key: this.form.seo_key.val,
+        overview: this.overviewEditorData,
+        detail: this.detailEditorData,
+        parameter: this.parameterEditorData,
+        sub_images: this.sub_images
+      }
+      return data
+    }
+  }
+
+
+}
+</script>
+
+<template>
+  <div class="w-full p-2">
+    <rounded-title class="text-xl flex ">
+      <span>{{ isEdit? "编辑产品" : "新增产品" }}</span>
+      <a href="/manger/product"
+         class="edit-btn custom-btn btn-13 ml-2">
+        产品列表
+      </a>
+    </rounded-title>
+
+    <div class="page-content">
+      <input-row class="mt-3"
+                 :msg="form.type.msg"
+                 :label-width="120"
+                 label="产品分类">
+        <table-select
+          class="w-48 flex-shrink-0"
+          :options="form.type.options"
+          v-model="form.type.val"
+        />
+      </input-row>
+
+      <input-row class="mt-3"
+                 :msg="form.name.msg"
+                 :label-width="120"
+                 label="产品名称">
+        <a-input class="!w-48 "
+                 v-model="form.name.val"
+                 placeholder="请输入产品名称"></a-input>
+      </input-row>
+
+      <input-row class="mt-3"
+                 :msg="form.remark.msg"
+                 :label-width="120"
+                 label="产品简述">
+        <a-input
+          v-model="form.remark.val"
+          placeholder="请输入产品简述"></a-input>
+      </input-row>
+
+      <input-row class="mt-3"
+                 :msg="form.sort.msg"
+                 :label-width="120"
+                 label="产品排序优先级">
+        <a-input-number class="!w-48 "
+                 v-model="form.sort.val"
+                  :min="0"
+                  :max="9999"
+        ></a-input-number>
+      </input-row>
+
+      <input-row class="mt-3"
+                 :msg="form.seo_key.msg"
+                 :label-width="120"
+                 label="seo关键字"
+                 remark="用于增加产品再搜索引擎中的关键字, 要确保准确有效"
+      >
+        <a-tooltip>
+          <template slot="title">
+            搜索引擎关注的关键字,多个用英文逗号隔开
+          </template>
+          <a-input v-model="form.seo_key.val" placeholder="seo关键字"></a-input>
+        </a-tooltip>
+        <div class="tags">
+          <a-tag
+            v-for="tag in tags"
+            :key="tag"
+            color="#2db7f5"
+            closable @close="deleteTag"
+          >
+            {{ tag }}
+          </a-tag>
+        </div>
+      </input-row>
+
+      <input-row class="mt-3"
+                 :msg="form.image.msg"
+                 :label-width="120"
+                 label="产品主图"
+                 remark="也会作为产品会显示的的封面"
+      >
+        <div class="rounded relative" style="width: 524px;height: 360px" >
+          <image-viewer class="" :src="imagePathBabel(form.image.showText)"
+                        :alt="form.image.showText" ></image-viewer>
+          <div class="absolute w-full h-full left-0 top-0
+                justify-center text-white bg-gray-400
+                items-center text-2xl flex opacity-0 hover:opacity-70"
+               @click="openImageSelect(imageSelectType.main)"
+          >
+            点击选择产品图片
+          </div>
+        </div>
+      </input-row>
+
+      <input-row class="mt-3"
+                 :label-width="120"
+                 label="产品子图"
+                 remark="产品更多的缩略图, 可以为空"
+      >
+        <div class="sub-image">
+<!--          新增按钮 -->
+          <div class="image-add">
+            <a-button type="primary" @click="addSubImage">
+              <a-icon type="plus" />
+              添加子图
+            </a-button>
+          </div>
+
+          <div class="image-list">
+            <div class="image-item"
+              v-for="(item, i) in sub_images"
+              :key="`sub_img_${item}`"
+            >
+              <div class="image-item-content">
+                <image-viewer
+                  :src="imagePathBabel(item)"
+                ></image-viewer>
+                <div class="absolute w-full h-full left-0 top-0
+                justify-center text-white bg-gray-400
+                items-center text-xl flex opacity-0 hover:opacity-70"
+                       @click="openImageSelect(imageSelectType.sub, i)"
+                  >
+                    选择图片
+                  </div>
+              </div>
+              <div class="option">
+                <a-button type="danger"
+                          @click="deleteSubImage(i)">
+                  移除
+                </a-button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </input-row>
+
+      <input-row class="mt-3"
+                 :label-width="120"
+                 label="产品简介"
+                 remark="产品的简要介绍"
+      >
+        <ckeditor class="mt-2" ref="ckeditor"
+                  :editor="overviewEditor"
+                  v-model="overviewEditorData"
+                  :config="overviewEditorConfig"></ckeditor>
+      </input-row>
+
+      <input-row class="mt-3"
+                 :label-width="120"
+                 label="产品详情"
+                 remark="产品的详细介绍"
+      >
+        <ckeditor class="mt-2" ref="ckeditor"
+                  :editor="detailEditor"
+                  v-model="detailEditorData"
+                  :config="detailEditorConfig"></ckeditor>
+      </input-row>
+
+      <input-row class="mt-3"
+                 :label-width="120"
+                 label="产品参数"
+                 remark="产品的参数配置信息, 推荐使用表格"
+      >
+        <ckeditor class="mt-2" ref="ckeditor"
+                  :editor="parameterEditor"
+                  v-model="parameterEditorData"
+                  :config="parameterEditorConfig"></ckeditor>
+      </input-row>
+
+      <div class="mt-2">
+        <a-button type="primary" @click="onSubmitHandle">
+          {{isEdit?"保存修改":"新增产品"}}
+        </a-button>
+        <a-button class="ml-2" @click="preview">
+          预览
+        </a-button>
+      </div>
+    </div>
+
+
+    <!--  图片选择弹窗 -->
+    <pop :show="imagePopShow" :loading="imagePopLoading">
+      <pop-card>
+        <template slot="header">
+          <span>选择需要插入的图片</span>
+        </template>
+        <image-table
+          class="w-full h-full"
+          @cancel="imagePopShow = false"
+          @ok="selectImageHandle">
+        </image-table>
+      </pop-card>
+    </pop>
+
+    <pop :show="previewShow" :loading="previewLoading"
+    >
+      <div class="product-show">
+<!--        关闭按钮 -->
+
+        <product-info
+          :product="previewData"
+          :unescape="false"
+          lang="lang"
+          :productId="editId"
+        />
+        <div class="absolute top-0 right-0 p-2 text-red-500 text-xl">
+          <a-icon type="close" @click="cancelPreview"/>
+        </div>
+      </div>
+    </pop>
+  </div>
+</template>
+
+<style scoped>
+.page-content{
+  margin-top: 20px;
+  width: 100%;
+  height: calc(100% - 50px);
+  overflow: auto;
+  background-color: #fff;
+  border-radius: 10px;
+  box-sizing: border-box;
+  padding: 10px;
+}
+.edit-btn{
+  font-size: 14px;
+}
+.tags{
+  width: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-start;
+  align-items: center;
+  padding: 5px;
+  box-sizing: border-box;
+}
+
+
+
+.sub-image{
+  width: 100%;
+  max-width: 520px;
+  height: auto;
+  padding: 0.5rem;
+  border-radius: 10px;
+  box-sizing: border-box;
+  background-color: #f5f5f5;
+  border: 1px solid #e5e5e5;
+}
+.image-add{
+  width: 100%;
+  height: 40px;
+  border-bottom: 1px solid #e5e5e5;
+}
+.image-list{
+  width: 100%;
+  height: auto;
+  max-height: 550px;
+  overflow: auto;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+.image-item{
+  width: calc(100% - 10px);
+  margin: 5px 0;
+  height: 100px;
+  display: flex;
+  justify-content: space-between;
+}
+.image-item-content{
+  width: 100px;
+  height: 100px;
+  position: relative;
+}
+.option{
+  width: 100px;
+  height: 100px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.product-show{
+  width: 80%;
+  height: calc(100% - 50px);
+  overflow: auto;
+  background-color: #fff;
+  border-radius: 10px;
+  position: relative;
+}
+</style>

+ 84 - 0
pages/manger/product/edit.vue

@@ -0,0 +1,84 @@
+<script >
+import langMap from "@/map/langMap";
+import {apiMap} from "@/map/apiMap";
+import {handle} from "@/until/handle";
+import {rCode} from "@/map/rcodeMap_esm";
+import {unescapeHtml} from "@/until/unescapeHtml";
+import {timestampToTime} from "@/until/time";
+import RoundedTitle from "@/components/public/roundedTitle.vue";
+import productAdd from "@/pages/manger/product/add.vue"
+export default {
+  name: "productInfoView",
+  components: {RoundedTitle, productAdd},
+  data() {
+    return {
+      loading: false,
+      productInfo: {},
+      // 英文的产品信息
+      productId: '',
+      lang: langMap.lang.zh,
+      showImage: ''
+    }
+  },
+  beforeMount() {
+    this.productId = this.$route.query.id;
+    this.loadProductInfo();
+  },
+  computed: {
+  },
+  methods: {
+    async loadProductInfo() {
+      let productId = this.productId;
+      if (!productId) {
+        this.$message.error('未获取到产品id');
+        return;
+      }
+      this.loading = true;
+      let url = apiMap.productInfo.path;
+      url += `/${productId}?lang=${this.lang}`
+      let [err, res] = await handle(this.$axios.get(url));
+      this.loading = false;
+      if (err) {
+        this.$message.error('获取产品信息失败');
+        return console.log(err);
+      }
+      let result = res.data;
+      if (result.code !== rCode.OK) {
+        this.$message.error(`获取产品信息失败,${result.msg}`)
+        return
+      }
+      console.log(result)
+      let data = result.data;
+      data.showImage = data.image;
+      this.productInfo = data;
+    },
+    getLangText(str) {
+      return langMap.getText(this.lang, str);
+    },
+  }
+}
+</script>
+
+<template>
+  <product-add :product="productInfo" :lang="lang" />
+</template>
+
+<style scoped>
+.page{
+  width: 100%;
+  height: calc(100% - 60px);
+}
+.product-show{
+  margin-top: 20px;
+  width: 100%;
+  height: calc(100% - 50px);
+  overflow: auto;
+  background-color: #fff;
+  border-radius: 10px;
+}
+.edit-btn{
+  font-size: 14px;
+}
+
+
+</style>

+ 4 - 1
pages/manger/product/index.vue

@@ -192,6 +192,9 @@ export default {
       let type = this.productTypes.find(item=>item.key === key);
       return type ? type.text : '';
     },
+    imagePathBabel(text){
+      return text?text.charAt(0) == '/'? text : '/public/'+text : ''
+    }
 
   }
 }
@@ -237,7 +240,7 @@ export default {
           </template>
 
           <template slot="image" slot-scope="text,record">
-            <image-viewer :src="text?text.charAt(0) == '/'? text : '/public/'+text : '' " :alt="record.name" />
+            <image-viewer :src="imagePathBabel(text)" :alt="record.name" />
           </template>
 
           <template slot="keyLight" class="flex" slot-scope="text">

+ 140 - 0
pages/manger/product/info.vue

@@ -0,0 +1,140 @@
+<script >
+import langMap from "@/map/langMap";
+import {apiMap} from "@/map/apiMap";
+import {handle} from "@/until/handle";
+import {rCode} from "@/map/rcodeMap_esm";
+import {unescapeHtml} from "@/until/unescapeHtml";
+import {timestampToTime} from "@/until/time";
+import RoundedTitle from "@/components/public/roundedTitle.vue";
+
+export default {
+  name: "productInfoView",
+  components: {RoundedTitle},
+  data() {
+    return {
+      loading: false,
+      productInfo: {},
+      // 英文的产品信息
+      productId: '',
+      lang: langMap.lang.zh,
+      showImage: ''
+    }
+  },
+  beforeMount() {
+    this.productId = this.$route.query.id;
+    this.loadProductInfo();
+  },
+  computed: {
+  },
+  methods: {
+    async loadProductInfo() {
+      let productId = this.productId;
+      if (!productId) {
+        this.$message.error('未获取到产品id');
+        return;
+      }
+      this.loading = true;
+      let url = apiMap.productInfo.path;
+      url += `/${productId}?lang=${this.lang}`
+      let [err, res] = await handle(this.$axios.get(url));
+      this.loading = false;
+      if (err) {
+        this.$message.error('获取产品信息失败');
+        return console.log(err);
+      }
+      let result = res.data;
+      if (result.code !== rCode.OK) {
+        this.$message.error(`获取产品信息失败,${result.msg}`)
+        return
+      }
+      let data = result.data;
+      data.showImage = data.image;
+      this.productInfo = result.data;
+    },
+    deleteProduct(){
+
+      this.$confirm({
+        title: '删除产品',
+        content: '是否删除该产品?',
+        okText: '删除',
+        okType: 'danger',
+        cancelText: '取消',
+        onOk: async () => {
+          this.executeDelete();
+        },
+      });
+    },
+    async executeDelete() {
+      let productId = this.productId;
+      if (!productId) {
+        this.$message.error('未获取到产品id');
+        return;
+      }
+      let url = apiMap.productDelete.path;
+      url += `?id=${productId}`
+      let [err, res] = await handle(this.$axios.post(url, {
+        id: productId
+      }));
+      if(err){
+        this.$message.error(`删除产品失败, 出现异常:${err.message}`);
+        return console.log(err);
+      }
+      let result = res.data;
+      if (result.code !== rCode.OK) {
+        this.$message.error(`删除产品失败,${result.msg}`)
+        console.log(result)
+        return
+      }
+      this.$message.success('删除产品成功');
+      window.location.href = '/manger/product';
+    },
+    getLangText(str) {
+      return langMap.getText(this.lang, str);
+    },
+  }
+}
+</script>
+
+<template>
+<div class="page p-2">
+  <rounded-title class="text-xl flex justify-between">
+    <a href="/manger/product">产品详情</a>
+
+    <a :href="`/manger/product/edit?id=${productInfo.proid}`"
+       class="edit-btn custom-btn btn-13">
+      编辑
+    </a>
+    <a-button type="danger" @click="deleteProduct">
+      删除产品
+    </a-button>
+  </rounded-title>
+  <div class="product-show">
+    <product-info
+      :product="productInfo"
+      :lang="lang"
+      :productId="productId"
+    />
+  </div>
+
+</div>
+</template>
+
+<style scoped>
+.page{
+  width: 100%;
+  height: calc(100% - 60px);
+}
+.product-show{
+  margin-top: 20px;
+  width: 100%;
+  height: calc(100% - 50px);
+  overflow: auto;
+  background-color: #fff;
+  border-radius: 10px;
+}
+.edit-btn{
+  font-size: 14px;
+}
+
+
+</style>

+ 52 - 5
pages/news/_type.vue

@@ -1,15 +1,16 @@
 <template>
 <!--  <product-index :p-type="type" :p-product="products"/>-->
 <!--  <solution-index :p-type="type" :p-solution="solutions"></solution-index>-->
-  <news-index :p-type="type" :p-news="solutions" :p-page-data="basePageData"></news-index>
+  <news-index :p-type="type" :p-news="news" :p-page-data="basePageData"></news-index>
 </template>
 
 <script>
-import productIndex from '@/pages/product/index.vue'
 import {handle} from "~/until/handle";
 import axios from "axios";
-import SolutionIndex from "@/pages/solution/index";
+import NewsIndex from "@/pages/news/index";
+
 import {apiMap, baseUrl} from "~/map/apiMap";
+import {isEmpty} from "@/until/typeTool";
 export default {
   name: "typeNewsPage",
   props:[],
@@ -23,6 +24,24 @@ export default {
       console.log(err);
       return {};
     }
+    // 获取页面的seo优化关键字
+    let types = ctx.store.getters.pTypes;
+    let title = "合方圆-新闻中心"
+    let seo_key = "合方圆,合方圆科技,深圳合方圆";
+    let seoDescription = "合方圆科技新闻中心,摄像头行为";
+    // 判断是否有数据
+    if(!isEmpty(types)){
+      // console.log(`已经获取到类型数据`);
+      // 获取当前类型的数据
+      let typeData = types.find(item=>item.type_key === type);
+      // console.log(typeData);
+      if(typeData){
+        seo_key += `,合方圆${typeData.type_name},${typeData.type_name},${typeData.seo_key}`;
+        seoDescription = `深圳市合方圆科技${typeData.type_name}`;
+        title = `合方圆-${typeData.type_name}`;
+      }
+    }
+
     let result = res.data;
     if(result.code === 1){
       let pageData = {
@@ -34,13 +53,41 @@ export default {
       return {
         news: result.data,
         basePageData: pageData,
+        title,
+        seo_key,
+        seoDescription
       }
     }else{
-      return {products:[]}
+      return {
+        news:[],
+        title,
+        seo_key,
+        seoDescription
+      }
+    }
+  },
+  head(){
+    return {
+      title: this.title,
+      meta: [
+        {
+          hid: 'description',
+          name: 'description',
+          content: this.seoDescription
+        },
+        {
+          hid: 'keywords',
+          name: 'keywords',
+          content: this.seo_key
+        },
+      ]
     }
   },
   data(){
     return {
+      title: "",
+      seo_key: "合方圆,合方圆科技",
+      seoDescription: "合方圆科技,合天地方圆,新闻中心",
       type:  'all',
       basePageData: {},
       news: []
@@ -50,7 +97,7 @@ export default {
     this.type = this.$route.params.type?this.$route.params.type:'all';
   },
   components:{
-    SolutionIndex,
+    NewsIndex,
   }
 }
 </script>

+ 45 - 3
pages/news/index.vue

@@ -2,7 +2,7 @@
   <div class="content">
     <lucency-header :lang="lang" page-key="news" :is-phone="isPhone"/>
     <item-banner :lang="lang" :title="`新闻中心`" :sub-title="`行业资讯,高新技术一应俱全`"/>
-    <news-types :lang="lang" :type="type"></news-types>
+    <show-types :lang="lang" :type="type" :types="types"></show-types>
     <solution-list :lang="lang" :solution-list="news" :parent-type="'news'"></solution-list>
     <page-select :page="page" :count="nowCount" :total="nowTotal"></page-select>
 
@@ -29,6 +29,32 @@ export default {
   name: "newIndex",
   components: {DefaultFooter, ItemBanner, LucencyHeader},
   props:['uLang','pType','pKey','pNews','pPageData'],
+  head(){
+    let headInfo = {
+      meta: []
+    }
+    if (this.title)
+    {
+      headInfo.title = this.title;
+    }
+    if (this.seoDescription)
+    {
+      headInfo.meta.push({
+        hid: "description",
+        name: "description",
+        content: this.seoDescription,
+      })
+    }
+    if(this.seo_key)
+    {
+      headInfo.meta.push({
+        hid: "keywords",
+        name: "keywords",
+        content: this.seo_key,
+      })
+    }
+    return headInfo
+  },
   async asyncData(ctx){
     let url = baseUrl + apiMap.searchNews.path;
     url += `?type=all&p=1`
@@ -37,6 +63,10 @@ export default {
       console.log(err);
       return {};
     }
+    let title = "合方圆-新闻中心"
+    let seo_key = "合方圆,合方圆科技,合方圆新闻";
+    let seoDescription = "合方圆科技最新新闻,公司动态,产品应用";
+
     let result = res.data;
     if(result.code === 1){
       let pageData = {
@@ -46,19 +76,31 @@ export default {
         count: result.count,
       }
       return {
+        title,
+        seo_key,
+        seoDescription,
         news: result.data,
         basePageData: pageData,
       }
     }else{
       console.error(result.msg);
       console.log(result);
-      return {news:[]}
+      return {
+        title,
+        seo_key,
+        seoDescription,
+        news:[]
+      }
     }
   },
   data(){
     return {
+      title: '',
+      seo_key: '',
+      seoDescription: '',
       lang: this.uLang?this.uLang:langMap.lang.cn,
       type: this.pType?this.pType:'all',
+      types: this.$store.getters.newsTypes,
       key: this.pKey?this.pKey:'',
       page: 1,
       nowCount: 199,
@@ -83,7 +125,7 @@ export default {
   mounted() {
     this.isPhone = isMediaView(0,1024);
     this.$root.$on('changeLang',this.switchLang);
-    this.$root.$on('changeNewsType',this.selectType);
+    this.$root.$on('changeType',this.selectType);
     this.$root.$on('changePage',this.changePageHandle);
     // this.$root.$on('changeProductType',this.selectType);
   },

+ 41 - 8
pages/news/info/_type.vue

@@ -15,18 +15,31 @@ export default {
   components: {ItemIndex},
   data(){
     return {
+      title: ``,
+      desc: '',
+      enDesc: '',
+      seo_key: '',
       type:  '',
       pId: 0,
       solutionInfo: []
     }
   },
-  beforeMount() {
-    // console.log('动态页面执行');
-    this.type = this.$route.params.type;
-    if(!this.type){
-      return window.location.href = '/news'
+  head(){
+    return {
+      title: this.title,
+      meta: [
+        {
+          hid: "description",
+          name: "description",
+          content: this.desc,
+        },
+        {
+          hid: 'keywords',
+          name: 'keywords',
+          content: this.seo_key,
+        },
+      ]
     }
-    // console.log(this.type);
   },
   async asyncData(ctx){
     let err,res;
@@ -44,14 +57,34 @@ export default {
     }
     let result = res.data;
     if(result.code === 1){
-      console.log(result.data);
-      return {pId:id, solutionInfo:result.data}
+      // console.log(result.data);
+      let desc = result.data.remark
+      let enDesc = result.data.remark_en
+      let seoKey = result.data.seo_key
+
+      return {
+        desc: desc,
+        enDesc: enDesc,
+        seo_key: seoKey? seoKey : `合方圆-产品中心,合方圆科技-新闻中心`,
+        title: `合方圆-${result.data.title}`,
+        pId:id,
+        solutionInfo:result.data
+      }
     }else{
       console.log(`查询数据失败,服务器异常`);
       ctx.redirect(`/news`);
     }
     return {pId:id}
   },
+  beforeMount() {
+    // console.log('动态页面执行');
+    this.type = this.$route.params.type;
+    if(!this.type){
+      return window.location.href = '/news'
+    }
+    // console.log(this.type);
+  },
+
 }
 </script>
 

+ 60 - 1
pages/news/info/index.vue

@@ -3,6 +3,7 @@
     <lucency-header :lang="lang" page-key="news" :is-phone="isPhone" />
     <item-banner :title="'合方圆'" :sub-title="solutionDetail.title"></item-banner>
     <big-title>
+      <a :href="`/news/${type.type_key}`">{{type.type_name}}</a>-
       {{solutionDetail.title}}
       <span class="author">
         {{solutionDetail.author}}
@@ -43,6 +44,32 @@ export default {
   name: "newsItemIndex",
   props:['uLang','pType', 'pInfo', "pId"],
   components:{ItemBanner, LucencyHeader, BigTitle, defaultFooter},
+  head() {
+    let headInfo = {
+      meta: []
+    }
+    if (this.title)
+    {
+      headInfo.title = this.title;
+    }
+    if (this.desc)
+    {
+      headInfo.meta.push({
+        hid: "description",
+        name: "description",
+        content: this.desc,
+      })
+    }
+    if(this.seo_key)
+    {
+      headInfo.meta.push({
+        hid: "keywords",
+        name: "keywords",
+        content: this.seo_key,
+      })
+    }
+    return headInfo
+  },
   async asyncData(ctx){
     let err,res;
     let id = ctx.query.id;
@@ -61,7 +88,17 @@ export default {
     let result = res.data;
     if(result.code === 1){
       console.log(result.data);
-      return {solutionId:id, solutionDetail:result.data}
+      let desc = result.data.remark
+      let enDesc = ctx.lang !== langMap.lang.cn ? result.data.remark_en || result.data.remark : result.data.remark
+      let seoKey = result.data.seo_key
+      return {
+        desc: desc,
+        enDesc: enDesc,
+        seo_key: seoKey? seoKey : `合方圆-新闻中心,合方圆科技-新闻中心`,
+        title: `合方圆-${result.data.title}`,
+        solutionId:id,
+        solutionDetail:result.data
+      }
     }else{
       console.log(`查询数据失败,服务器异常`);
       ctx.redirect(`/news`);
@@ -70,6 +107,10 @@ export default {
   },
   data(){
     return {
+      title: ``,
+      desc: '',
+      enDesc: '',
+      seo_key: '',
       langType: langMap.lang,
       lang: this.uLang?this.uLang:langMap.lang.cn,
       solutionId: null,
@@ -80,6 +121,24 @@ export default {
       isPhone: false,
     }
   },
+  computed: {
+    type(){
+      let info = isEmpty(this.solutionDetail)?this.pInfo:this.solutionDetail
+      let types = this.$store.getters.allNewsTypes || this.types;
+      let type = types.find(val => val.type_key === info.type_key);
+
+      let resultType = {}
+      if(this.lang !== langMap.lang.cn)
+      {
+        resultType.type_name = type.type_name_en;
+        resultType.sub_text = type.sub_text_en;
+      }
+      if(!resultType.type_name) resultType.type_name = type.type_name;
+      if(!resultType.sub_text) resultType.sub_text = type.sub_text;
+      resultType.type_key = type.type_key;
+      return resultType;
+    }
+  },
   beforeMount() {
     console.log(this.pInfo);
     if(isEmpty(this.pInfo) && isEmpty(this.solutionDetail)){

+ 21 - 15
pages/product/_type.vue

@@ -3,7 +3,7 @@
     :p-type="type"
     :p-product="products"
     :p-page-data="basePageData"
-    :p-seo-key="seoKey"
+    :p-seo-key="seo_key"
     :p-seo-description="seoDescription"
   />
 </template>
@@ -29,21 +29,24 @@ export default {
       console.log(err);
       return {};
     }
-    // 获取 nuxtServerInit 中的数据 getters productTypes
-    let productTypes = ctx.store.getters.productTypes;
-    let seoKey = "合方圆,合方圆科技";
+    // 获取页面的seo优化关键字
+    let types = ctx.store.getters.pTypes;
+    let title = "合方圆-产品中心"
+    let seo_key = "合方圆,合方圆科技,深圳合方圆";
     let seoDescription = "合方圆科技产品中心,可选择4g,或者poe摄像头.满足多种需求,以及定制化需求";
     // 判断是否有数据
-    if(!isEmpty(productTypes)){
-      console.log(`已经获取到类型数据`);
+    if(!isEmpty(types)){
+      // console.log(`已经获取到类型数据`);
       // 获取当前类型的数据
-      let typeData = productTypes.find(item=>item.typeKey === type);
-      console.log(typeData);
+      let typeData = types.find(item=>item.type_key === type);
+      // console.log(typeData);
       if(typeData){
-        seoKey = `合方圆,合方圆科技,深圳合方圆,${typeData.text},合方圆${typeData.text}`;
-        seoDescription = `深圳市合方圆科技${typeData.text}产品`;
+        seo_key += `,合方圆${typeData.type_name},${typeData.type_name},${typeData.seo_key}`;
+        seoDescription = `深圳市合方圆科技${typeData.type_name}产品`;
+        title = `合方圆-${typeData.type_name}产品`;
       }
     }
+
     let result = res.data;
     if(result.code === 1){
       let pageData = {
@@ -55,20 +58,22 @@ export default {
       return {
         products: result.data,
         basePageData: pageData,
-        seoKey,
+        title,
+        seo_key,
         seoDescription
       }
     }else{
       return {
         products:[],
-        seoKey,
+        title,
+        seo_key,
         seoDescription
       }
     }
   },
   head(){
     return {
-      title: this.seoKey,
+      title: this.title,
       meta: [
         {
           hid: 'description',
@@ -78,7 +83,7 @@ export default {
         {
           hid: 'keywords',
           name: 'keywords',
-          content: this.seoKey
+          content: this.seo_key
         },
       ]
     }
@@ -87,7 +92,8 @@ export default {
     return {
       type:  'all',
       basePageData: {},
-      seoKey: "合方圆,合方圆科技",
+      title: "",
+      seo_key: "合方圆,合方圆科技",
       seoDescription: "合方圆科技,合天地方圆,产品中心",
       products: []
     }

+ 45 - 18
pages/product/index.vue

@@ -10,6 +10,7 @@ import {isMediaView} from "@/until/mediaView";
 import LucencyHeader from "~/components/header/lucencyHeader.vue";
 import DefaultFooter from "~/components/footer/defaultFooter.vue";
 import {apiMap, baseUrl} from "~/map/apiMap";
+import {isEmpty} from "@/until/typeTool";
 export default {
   name: "index",
   props:[
@@ -22,21 +23,30 @@ export default {
     productBanner
   },
   head(){
-    return {
-      title: `合方圆-产品中心`,
-      meta: [
-        {
-          hid: 'description',
-          name: 'description',
-          content: this.pSeoDescription? this.pSeoDescription : `合方圆科技产品中心,可选择4g,或者poe摄像头.满足多种需求,以及定制化需求`
-        },
-        {
-          hid: 'keywords',
-          name: 'keywords',
-          content: this.pSeoKey? this.pSeoKey : `合方圆-产品中心,合方圆科技-产品中心`
-        },
-      ]
+    let headInfo = {
+      meta: []
+    }
+    if (this.title)
+    {
+      headInfo.title = this.title;
+    }
+    if (this.seoDescription)
+    {
+      headInfo.meta.push({
+        hid: "description",
+        name: "description",
+        content: this.seoDescription,
+      })
     }
+    if(this.seo_key)
+    {
+      headInfo.meta.push({
+        hid: "keywords",
+        name: "keywords",
+        content: this.seo_key,
+      })
+    }
+    return headInfo
   },
   async asyncData(ctx){
     // 获取数据
@@ -48,6 +58,14 @@ export default {
       return {};
     }
     let result = res.data;
+
+    // 获取页面的seo优化关键字, 直接访问首页不需要获取对应的seo关键字
+    let types = ctx.store.getters.pTypes;
+    let title = "合方圆-产品中心"
+    let seo_key = "合方圆,合方圆科技,深圳合方圆";
+    let seoDescription = "合方圆科技产品中心,可选择4g,或者poe摄像头.满足多种需求,以及定制化需求";
+
+
     // 获取seo 数据
     if(result.code === 1){
       let pageData = {
@@ -57,20 +75,29 @@ export default {
         count: result.count,
       }
       return {
+        title,
+        seo_key,
+        seoDescription,
         products: result.data,
         basePageData: pageData,
       }
     }else{
       console.error(result.msg);
       console.log(result);
-      return {products:[]}
+      return {
+        products:[],
+        title,
+        seo_key,
+        seoDescription
+      }
     }
   },
   data(){
     return {
       lang: this.uLang?this.uLang:langMap.lang.cn,
-      seoKey: '合方圆-产品中心,合方圆科技-产品中心',
-      seoDescription: '深圳合方圆科技产品中心',
+      title: '',
+      seo_key: '',
+      seoDescription: '',
       type: this.pType?this.pType:'all',
       key: this.pKey?this.pKey:'',
       page: 1,
@@ -184,7 +211,7 @@ export default {
     <!--    推荐广告-->
     <product-banner :lang="lang" />
     <!--    产品类别 -->
-    <product-types :lang="lang" :type="type" :types="types" @changeType="changeTypeHandle"></product-types>
+    <show-types :lang="lang" :type="type" :types="types" @changeType="changeTypeHandle"></show-types>
     <!--    产品列表 -->
     <product-list :lang="lang" :product-list="products"></product-list>
     <page-select :page="page" :count="nowCount" :total="nowTotal"></page-select>

+ 26 - 4
pages/product/info/_type.vue

@@ -8,11 +8,16 @@ import {apiMap, baseUrl} from "~/map/apiMap";
 import {handle} from "~/until/handle";
 import axios from "axios";
 import {toNumber} from "~/until/typeTool";
+import langMap from "@/map/langMap";
 export default {
   name: "itemType",
   components: {ItemIndex},
   data(){
     return {
+      title: ``,
+      desc: '',
+      enDesc: '',
+      seo_key: '',
       type:  '',
       productionInfo: {},
       loadErr: false,
@@ -20,10 +25,18 @@ export default {
   },
   head(){
     return {
-      title: `合方圆-${this.productionInfo.name}`,
+      title: this.title,
       meta: [
-        { hid: 'description', name: 'description', content: this.productionInfo.description },
-        { hid: 'keywords', name: 'keywords', content: this.productionInfo.keywords },
+        {
+          hid: "description",
+          name: "description",
+          content: this.desc,
+        },
+        {
+          hid: 'keywords',
+          name: 'keywords',
+          content: this.seo_key,
+        },
       ]
     }
   },
@@ -45,7 +58,16 @@ export default {
     let result = res.data;
     if(result.code === 1){
       // console.log(result.data);
-      return {productionInfo:result.data}
+      let desc = result.data.remark
+      let enDesc = result.data.remark_en
+      let seoKey = result.data.seo_key
+      return {
+        desc: desc,
+        enDesc: enDesc,
+        seo_key: seoKey? seoKey : `合方圆-产品中心,合方圆科技-产品中心`,
+        title: `合方圆-${result.data.name}`,
+        productionInfo:result.data
+      }
     }else{
       console.log(`查询数据失败,服务器异常`);
       ctx.redirect(`/product`);

+ 105 - 39
pages/product/info/index.vue

@@ -1,42 +1,35 @@
 <template>
   <div class="content">
     <lucency-header :lang="lang" page-key="product" :is-phone="isPhone" />
-    <item-banner :title="productTypeText" :sub-title="productTypeSubText"></item-banner>
-    <big-title>{{productDetail.name}}</big-title>
-    <div class="conBox product-view">
-      <div class="left">
-        <div class="imgView">
-          <img v-show="productDetail.image" :src="`/public/${productDetail.image}`" alt=""/>
-        </div>
-        <div class="concatUs">
-<!--          联系销售 -->
-          <span class="chunk">联系购买</span>
-          <div class="imgView">
-            <img src="/image/wechat.jpg" alt="">
-          </div>
-        </div>
-      </div>
-      <div class="right">
-        <div class="remark">
-          {{productDetail.remark}}
-        </div>
-        <div class="detail" v-html="productDetail.detail">
-        </div>
-      </div>
-    </div>
-<!--    更多数据页面 -->
-    <div class="conBox product-detail">
-      <q-tab></q-tab>
-    </div>
+    <item-banner :title="type.type_name" :sub-title="type.sub_text"></item-banner>
+    <big-title>
+      <a :href="`/product/${type.type_key}`">{{type.type_name}}</a>-
+      {{productDetail.name}}
+    </big-title>
 
+    <div class="container product-view mx-auto ">
+      <product-info :product="productDetail" :lang="lang" :unescape="true" @tryBuy="tryBuy"/>
+    </div>
 <!--    页脚 -->
     <default-footer :lang="lang"/>
 
     <site-bar wechat-src="/image/wechat.jpg"></site-bar>
+
+
+    <pop :show="buyShow" :loading="buyLoading"
+    >
+      <div class="product-show">
+        <div class="absolute top-0 right-0 p-2 text-red-500 text-xl">
+          <a-icon type="close" @click="cancelBuy"/>
+        </div>
+      </div>
+    </pop>
   </div>
 </template>
 
 <script>
+// import 'ant-design-vue/dist/antd.css'
+
 import axios from "axios";
 import langMap from "~/map/langMap";
 import { getTypeText,getTypeSubText, pTypes} from "~/map/productMap";
@@ -50,12 +43,40 @@ import {isMediaView} from "@/until/mediaView";
 import {apiMap, baseUrl} from "~/map/apiMap";
 import BigTitle from "~/components/public/bigTitle.vue";
 import {toNumber,isEmpty} from "../../../until/typeTool";
+import Pop from "@/components/public/pop.vue";
 
 export default {
   name: "itemIndex",
-  components: {BigTitle, ItemBanner, LucencyHeader, DefaultFooter, qTab},
+  components: {Pop, BigTitle, ItemBanner, LucencyHeader, DefaultFooter, qTab},
   props:['uLang','pType', 'pInfo'],
+  head() {
+    let headInfo = {
+      meta: []
+    }
+    if (this.title)
+    {
+      headInfo.title = this.title;
+    }
+    if (this.desc)
+    {
+      headInfo.meta.push({
+        hid: "description",
+        name: "description",
+        content: this.desc,
+      })
+    }
+    if(this.seo_key)
+    {
+      headInfo.meta.push({
+        hid: "keywords",
+        name: "keywords",
+        content: this.seo_key,
+      })
+    }
+    return headInfo
+  },
   async asyncData(ctx){
+    console.log(`asyncData `);
     // 判断是否有id,如果有id则请求数据,否则则不请求数据
     let err,res;
     let id = ctx.query.id;
@@ -75,10 +96,17 @@ export default {
     }
     let result = res.data;
     if(result.code === 1){
-      // console.log(result.data);
+      console.log('产品详情');
+      console.log(result.data);
+      let desc = result.data.remark
+      let enDesc = ctx.lang !== langMap.lang.cn ? result.data.remark_en || result.data.remark : result.data.remark
+      let seoKey = result.data.seo_key
       return {
-        productDetail:result.data,
-        type: result.data.type_key,
+        desc: desc,
+        enDesc: enDesc,
+        seo_key: seoKey? seoKey : `合方圆-产品中心,合方圆科技-产品中心`,
+        title: `合方圆-${result.data.name}`,
+        productDetail:result.data
       }
     }else{
       console.log(`查询产品数据失败,服务器异常`);
@@ -88,17 +116,40 @@ export default {
   },
   data(){
     return {
+      title: ``,
+      desc: '',
+      enDesc: '',
+      seo_key: '',
       langType: langMap.lang,
       lang: this.uLang?this.uLang:langMap.lang.cn,
       productId: null,
-      types: pTypes,
-      type: '',
-      productTypeText: getTypeText(this.pType),
-      productTypeSubText: getTypeSubText(this.pType),
+      types: this.$store.getters.pTypes,
       productDetail: {},
       isPhone: false,
+      buyShow: false,
+      buyLoading: false,
+
+    }
+  },
+  computed: {
+    type(){
+      let info = isEmpty(this.productDetail)?this.pInfo:this.productDetail
+
+      let types = this.$store.getters.pTypes || this.types;
+      let type = types.find(val => val.type_key === info.type_key);
+      let resultType = {}
+      if(this.lang !== langMap.lang.cn)
+      {
+        resultType.type_name = type.type_name_en;
+        resultType.sub_text = type.sub_text_en;
+      }
+      if(!resultType.type_name) resultType.type_name = type.type_name;
+      if(!resultType.sub_text) resultType.sub_text = type.sub_text;
+      resultType.type_key = type.type_key;
+      return resultType;
     }
   },
+
   beforeMount() {
     console.log(this.pInfo);
     if(isEmpty(this.productDetail) && isEmpty(this.pInfo)){
@@ -109,15 +160,12 @@ export default {
     if(isEmpty(this.productDetail)){
       this.productDetail = this.pInfo;
     }
-    this.productDetail.detail = unescape(this.productDetail.detail);
   },
   mounted() {
     // console.log(this.pType);
     // console.log(this.type);
+    // this.$root.$on('tryBuy',this.tryBuy);
     this.isPhone = isMediaView(0,1024);
-    if(!isEmpty(this.type)){
-      this.type = this.pType;
-    }
 
   },
   methods:{
@@ -148,6 +196,14 @@ export default {
         return alert('加载产品失败!!!');
       }
     },
+    tryBuy(){
+      console.log('tryBuy');
+      this.buyShow = true;
+
+    },
+    cancelBuy(){
+      this.buyShow = false;
+    },
 
   }
 }
@@ -222,4 +278,14 @@ export default {
 .tab-header .header-title{
 
 }
+
+
+.product-show{
+  width: 80%;
+  height: calc(100% - 50px);
+  overflow: auto;
+  background-color: #fff;
+  border-radius: 10px;
+  position: relative;
+}
 </style>

+ 49 - 2
pages/solution/_type.vue

@@ -4,12 +4,12 @@
 </template>
 
 <script>
-import productIndex from '@/pages/product/index.vue'
 import {handle} from "~/until/handle";
 import axios from "axios";
 import qs from "qs";
 import SolutionIndex from "@/pages/solution/index";
 import {apiMap, baseUrl} from "~/map/apiMap";
+import {isEmpty} from "@/until/typeTool";
 const pageLimit = 5;
 export default {
   name: "typeSolutionPage",
@@ -25,6 +25,25 @@ export default {
       return {};
     }
     let result = res.data;
+
+    // 获取页面的seo优化关键字
+    let types = ctx.store.getters.pTypes;
+    let title = "合方圆-解决方案"
+    let seo_key = "合方圆,合方圆科技,深圳合方圆";
+    let seoDescription = "合方圆科技产品中心,定制化需求, 电网解决方案, 国标应用";
+    // 判断是否有数据
+    if(!isEmpty(types)){
+      // console.log(`已经获取到类型数据`);
+      // 获取当前类型的数据
+      let typeData = types.find(item=>item.type_key === type);
+      // console.log(typeData);
+      if(typeData){
+        seo_key += `,合方圆${typeData.type_name},${typeData.type_name},${typeData.seo_key}`;
+        seoDescription = `深圳市合方圆科技${typeData.type_name}`;
+        title = `合方圆-${typeData.type_name}`;
+      }
+    }
+
     if(result.code === 1){
       let pageData = {
         limit: result.limit,
@@ -35,13 +54,41 @@ export default {
       return {
         solutions: result.data,
         basePageData: pageData,
+        title,
+        seo_key,
+        seoDescription
       }
     }else{
-      return {solutions:[]}
+      return {
+        solutions:[],
+        title,
+        seo_key,
+        seoDescription
+      }
+    }
+  },
+  head(){
+    return {
+      title: this.title,
+      meta: [
+        {
+          hid: 'description',
+          name: 'description',
+          content: this.seoDescription
+        },
+        {
+          hid: 'keywords',
+          name: 'keywords',
+          content: this.seo_key
+        },
+      ]
     }
   },
   data(){
     return {
+      title: "",
+      seo_key: "合方圆,合方圆科技",
+      seoDescription: "合方圆科技,合天地方圆,解决方案",
       type:  'all',
       basePageData: {},
       solutions: []

+ 45 - 9
pages/solution/index.vue

@@ -2,7 +2,8 @@
   <div class="">
     <lucency-header :lang="lang" page-key="solution" :is-phone="isPhone" />
     <item-banner :lang="lang" :title="`解决方案`" :sub-title="`专业,高效,稳定解决方案`"/>
-    <solution-types :lang="lang" :type="type"></solution-types>
+    <show-types :lang="lang" :type="type" :types="types"></show-types>
+
     <solution-list :lang="lang" :solution-list="solutions"></solution-list>
     <page-select :page="page" :count="nowCount" :total="nowTotal"></page-select>
 
@@ -41,13 +42,30 @@ export default {
     solutionList
   },
   head(){
-    return {
-      title: `合方圆-解决方案`,
-      meta: [
-        { hid: 'description', name: 'description', content: `合方圆-解决方案` },
-        { hid: 'keywords', name: 'keywords', content: `合方圆-解决方案,合方圆科技-解决方案,` },
-      ]
+    let headInfo = {
+      meta: []
+    }
+    if (this.title)
+    {
+      headInfo.title = this.title;
+    }
+    if (this.seoDescription)
+    {
+      headInfo.meta.push({
+        hid: "description",
+        name: "description",
+        content: this.seoDescription,
+      })
+    }
+    if(this.seo_key)
+    {
+      headInfo.meta.push({
+        hid: "keywords",
+        name: "keywords",
+        content: this.seo_key,
+      })
     }
+    return headInfo
   },
   async asyncData(ctx){
     // 获取数据
@@ -59,6 +77,11 @@ export default {
       return {};
     }
     let result = res.data;
+
+    let title = "合方圆-解决方案"
+    let seo_key = "合方圆-解决方案,合方圆科技-解决方案";
+    let seoDescription = "合方圆科技解决方案,针对不同应用场景定制不同的集中化方案";
+
     if(result.code === 1){
       let pageData = {
         limit: result.limit,
@@ -67,19 +90,31 @@ export default {
         count: result.count,
       }
       return {
+        title,
+        seo_key,
+        seoDescription,
         solutions: result.data,
         basePageData: pageData,
       }
     }else{
       console.error(result.msg);
       console.log(result);
-      return {solutions:[]}
+      return {
+        title,
+        seo_key,
+        seoDescription,
+        solutions:[]
+      }
     }
   },
   data(){
     return {
+      title: '',
+      seo_key: '',
+      seoDescription: '',
       lang: this.uLang?this.uLang:langMap.lang.cn,
       type: this.pType?this.pType:'all',
+      types: this.$store.getters.solutionTypes,
       key: this.pKey?this.pKey:'',
       page: 1,
       nowCount: 199,
@@ -102,7 +137,8 @@ export default {
   mounted() {
     this.isPhone = isMediaView(0,1024);
     this.$root.$on('changeLang',this.switchLang);
-    this.$root.$on('changeSolutionType',this.selectType);
+    // this.$root.$on('changeSolutionType',this.selectType);
+    this.$root.$on('changeType',this.selectType);
     this.$root.$on('changePage',this.changePageHandle);
     // this.$root.$on('changeProductType',this.selectType);
   },

+ 34 - 2
pages/solution/info/_type.vue

@@ -13,11 +13,32 @@ export default {
   components: {ItemIndex},
   data(){
     return {
+      title: ``,
+      desc: '',
+      enDesc: '',
+      seo_key: '',
       type:  '',
       pId: 0,
       solutionInfo: {}
     }
   },
+  head(){
+    return {
+      title: this.title,
+      meta: [
+        {
+          hid: "description",
+          name: "description",
+          content: this.desc,
+        },
+        {
+          hid: 'keywords',
+          name: 'keywords',
+          content: this.seo_key,
+        },
+      ]
+    }
+  },
   beforeMount() {
     // console.log('动态页面执行');
     this.type = this.$route.params.type;
@@ -42,8 +63,19 @@ export default {
     }
     let result = res.data;
     if(result.code === 1){
-      console.log(result.data);
-      return {pId:id, solutionInfo:result.data}
+      // console.log(result.data);
+      let desc = result.data.remark
+      let enDesc = result.data.remark_en
+      let seoKey = result.data.seo_key
+
+      return {
+        desc: desc,
+        enDesc: enDesc,
+        seo_key: seoKey? seoKey : `合方圆-产品中心,合方圆科技-解决方案`,
+        title: `合方圆-${result.data.title}`,
+        pId:id,
+        solutionInfo:result.data
+      }
     }else{
       console.log(`查询数据失败,服务器异常`);
       ctx.redirect(`/solution`);

+ 59 - 1
pages/solution/info/index.vue

@@ -3,6 +3,7 @@
     <lucency-header :lang="lang" page-key="solution" :is-phone="isPhone"/>
     <item-banner :title="productTypeText" :sub-title="productTypeSubText"></item-banner>
     <big-title>
+      <a :href="`/solution/${type.type_key}`">{{type.type_name}}</a>-
       {{solutionDetail.title}}
       <span class="author">
         {{solutionDetail.author}}
@@ -44,6 +45,32 @@ export default {
   name: "solutionItemIndex",
   props:['uLang', 'pType', 'pInfo', "pId"],
   components:{LucencyHeader, ItemBanner, BigTitle, defaultFooter},
+  head() {
+    let headInfo = {
+      meta: []
+    }
+    if (this.title)
+    {
+      headInfo.title = this.title;
+    }
+    if (this.desc)
+    {
+      headInfo.meta.push({
+        hid: "description",
+        name: "description",
+        content: this.desc,
+      })
+    }
+    if(this.seo_key)
+    {
+      headInfo.meta.push({
+        hid: "keywords",
+        name: "keywords",
+        content: this.seo_key,
+      })
+    }
+    return headInfo
+  },
   async asyncData(ctx){
     let err,res;
     let id = ctx.query.id;
@@ -62,7 +89,16 @@ export default {
     let result = res.data;
     if(result.code === 1){
       console.log(result.data);
-      return {solutionId:id, solutionDetail:result.data}
+      let desc = result.data.remark
+      let enDesc = ctx.lang !== langMap.lang.cn ? result.data.remark_en || result.data.remark : result.data.remark
+      let seoKey = result.data.seo_key
+
+      return {
+        desc: desc,
+        enDesc: enDesc,
+        seo_key: seoKey? seoKey : `合方圆,合方圆科技-解决方案`,
+        title: `合方圆-${result.data.title}`,
+        solutionId:id, solutionDetail:result.data}
     }else{
       console.log(`查询文章数据失败,服务器异常`);
       ctx.redirect(`/solution`);
@@ -71,6 +107,10 @@ export default {
   },
   data(){
     return {
+      title: ``,
+      desc: '',
+      enDesc: '',
+      seo_key: '',
       langType: langMap.lang,
       lang: this.uLang?this.uLang:langMap.lang.cn,
       solutionId: this.pId,
@@ -81,6 +121,24 @@ export default {
       isPhone: false
     }
   },
+  computed: {
+    type(){
+      let info = isEmpty(this.solutionDetail)?this.pInfo:this.solutionDetail
+
+      let types = this.$store.getters.allNewsTypes || this.types;
+      let type = types.find(val => val.type_key === info.type_key);
+      let resultType = {}
+      if(this.lang !== langMap.lang.cn)
+      {
+        resultType.type_name = type.type_name_en;
+        resultType.sub_text = type.sub_text_en;
+      }
+      if(!resultType.type_name) resultType.type_name = type.type_name;
+      if(!resultType.sub_text) resultType.sub_text = type.sub_text;
+      resultType.type_key = type.type_key;
+      return resultType;
+    }
+  },
   beforeMount() {
     console.log(this.pInfo);
     if(isEmpty(this.pInfo) && isEmpty(this.solutionDetail)){

+ 54 - 2
server/control/c_base.js

@@ -299,7 +299,8 @@ async function getBaseData(){
   // 同时获取所有需要获取的基础数据
   let err,pType,nType,carousel;
   // todo 同时获取所有需要获取的基础数据
-  [err,pType,nType,carousel] = await handleAll(
+  [err, baseInfo, pType, nType, carousel] = await handleAll(
+    d_base.getBaseInfo(),
     d_product.loadTypes(),
     d_news.loadTypes(),
     d_base.getCarousel({state: dbField.db_base.carouselState.enable}),
@@ -310,6 +311,7 @@ async function getBaseData(){
   }
   // 合并数据
   let baseData = {
+    baseInfo: baseInfo? baseInfo[0]: {},
     pType,
     nType,
     carousel,
@@ -319,6 +321,7 @@ async function getBaseData(){
     item.filePath = filePathToUrl(item.fileType,item.filePath);
     return item;
   });
+  // todo 缓存接口数据
   log.info(`[基础数据] 类型分类获取成功
                 程序类型数量:${pType.length}
                 文章类型数量:${nType.length}
@@ -326,6 +329,54 @@ async function getBaseData(){
                 `);
   return [null,baseData];
 }
+
+
+async function getBaseInfo(){
+  let err,res;
+  [err,res] = await handle(d_base.getBaseInfo());
+  if(err){
+    log.error(`[基础数据] 获取基础数据失败 ${err.message}`);
+    return [{eCode: codeMap.ServerError, eMsg: `获取基础数据失败`}, null];
+  }
+  if(!res.length){
+    log.error(`[基础数据] 获取基础数据失败,数据库中无基础数据`);
+    return []
+  }
+  res = res[0];
+  return [null,res];
+}
+
+async function editBaseInfo(data){
+  let err,res;
+  let baseId = data.id;
+  [err,res] = await handle(d_base.getBaseInfo(baseId));
+  if(err){
+    log.error(`[基础数据] 获取基础数据失败 ${err.message}`);
+    return [{eCode: codeMap.ServerError, eMsg: `获取基础数据失败`}, null];
+  }
+  if(!res.length){
+    log.error(`[基础数据] 编辑基础数据失败,数据库中无基础数据 尝试添加`);
+    // 新增数据
+    [err,res] = await handle(d_base.addBaseInfo(data));
+    if(err){
+      log.error(`[基础数据] 新增基础数据失败 ${err.message}`);
+      return [{eCode: codeMap.ServerError, eMsg: `新增基础数据失败`}, null];
+    }
+    log.info(`[基础数据] 新增基础数据成功`);
+    return [null,true];
+  }
+  let baseInfo = res[0];
+  // console.log(data);
+  [err,res] = await handle(d_base.editBaseInfo(data, baseInfo.id));
+  if(err){
+    log.error(`[基础数据] 编辑基础数据失败 ${err.message}`);
+    return [{eCode: codeMap.ServerError, eMsg: `编辑基础数据失败`}, null];
+  }
+  log.info(`[基础数据] 编辑基础数据成功`);
+  return [null,true];
+}
+
+
 module.exports = {
   getEnableCarousel,
   getAllCarousel,
@@ -336,7 +387,8 @@ module.exports = {
   searchFiles,
   deleteFile,
   getBaseData,
-
+  getBaseInfo,
+  editBaseInfo
 }
 
 

+ 1 - 1
server/control/c_solution.js

@@ -137,7 +137,7 @@ async function searchAllNews(pType,type, key, p, l){
     _params.parentType = dbField.db_base.newsType.solution;
   }else{
     // 不区分主类别
-    log.info(`pType 未知${pType} ${typeof pType}}`);
+    log.info(`pType 未知${pType} ${typeof pType}`);
   }
 
 

+ 100 - 9
server/control/product.js

@@ -27,27 +27,115 @@ async function loadProduct(key,p,l)
  * @param id 产品id
  * @returns {Promise<*[]>}
  */
-async function getProductInfo(id)
+async function getProductInfo(id, lang = 'zh')
 {
-  let [err,res] = await handle(d_product.getProductInfo(id));
+  log.info(`获取产品信息:${id}`);
+  let productInfoPromise = d_product.getProductInfo(id)
+  let [err,res] = await handle(productInfoPromise);
   if(err){
+    console.log(err)
     return [err,null];
   }
-
+  let products = res;
   let data = {};
-  if(res.length){
-    data = res[0];
+  if(products.length){
+    data = products[0];
   }else{
     return [
       {
-        eCode:codeMap.NotFound,
-        eMsg:`无法找到对应产品`
-      },null]
+        eCode: codeMap.NotFound,
+        eMsg: `无法找到对应产品`
+      }, null]
+  }
+  if(data.sub_img){
+    data.sub_images = data.sub_img.split(';');
+  }else {
+    data.sub_images = [];
   }
-  // console.log(data);
+
+  // todo 英文字段支持
   return [null,data];
 }
 
+async function addProduct(product)
+{
+
+  product.sub_img = product.sub_images.join(';');
+  // 获取产品分类
+  let [err,res] = await handle(d_product.getTypeByKey(product.type))
+  if(err){
+    log.error(`无法获取产品分类:${product.type}`)
+    log.error(err)
+    return [err,null];
+  }
+  if(!res.length){
+    log.error(`无法获取产品分类:${product.type}`)
+    return [
+      {
+        eCode: codeMap.NotFound,
+        eMsg: `无法找到对应产品分类`
+      }, null]
+  }
+  let typeId = res[0].type_id;
+  [err,res] = await handle(d_product.addProduct(product, typeId));
+  if(err){
+    log.error(`添加产品失败:${JSON.stringify(product)}`)
+    log.error(err)
+    return [err,null];
+  }
+  console.log(res)
+  return [null,res];
+}
+
+async function editProduct(product)
+{
+
+  product.sub_img = product.sub_images.join(';');
+  let [err,res] = await handle(d_product.getTypeByKey(product.type))
+  if(err){
+    log.error(`无法获取产品分类 ${product.type}`)
+    log.error(err)
+    return [err,null];
+  }
+  let typeId = res[0].type_id;
+
+  [err,res] = await handle(d_product.editProduct(product, typeId, product.proid));
+  if(err){
+    log.error(`更新产品失败:${JSON.stringify(product)}`)
+    log.error(err)
+    return [err,null];
+  }
+  return [null,res];
+
+}
+
+async function deleteProduct(id)
+{
+  // 寻找产品
+  let [err,res] = await handle(d_product.getProductInfo(id));
+  if(err){
+    log.error(`无法获取产品信息:${id}`)
+    log.error(err)
+    return [err,null];
+  }
+  if(!res.length){
+    log.error(`无法获取产品信息:${id}`)
+    return [
+      {
+        eCode: codeMap.NotFound,
+        eMsg: `无法找到对应产品`
+      }, null]
+  }
+
+  [err,res] = await handle(d_product.deleteProduct(id));
+  if(err){
+    log.error(`删除产品失败:${id}`)
+    log.error(err)
+    return [err,null];
+  }
+  return [null,res];
+}
+
 async function searchProduct(type, key, p, l)
 {
   p = p || 1;
@@ -178,6 +266,9 @@ async function deleteType(id) {
 module.exports = {
   loadProduct,
   getProductInfo,
+  addProduct,
+  editProduct,
+  deleteProduct,
   searchProduct,
   searchProductByMini,
   getProductTypes,

+ 61 - 1
server/database/d_base.js

@@ -145,6 +145,64 @@ function deleteFile(fileId){
   return mysql.pq(sql,[fileId]);
 }
 
+function getBaseInfo(id){
+  let sql = `select * from hfy_basic`;
+  if(id){
+    sql += ` where id = ? limit 1`;
+    return mysql.pq(sql,[id]);
+  }
+  return mysql.pq(sql);
+}
+
+function editBaseInfo(updateParam, baseId){
+  let sql = `update hfy_basic set `;
+  let values = [];
+  if(!isEmpty(updateParam.company)){
+    sql += ` company = ?,`;
+    values.push(updateParam.company);
+  }
+  if(!isEmpty(updateParam.wx_qrc)){
+    sql += ` wx_qrc = ?,`;
+    values.push(updateParam.wx_qrc);
+  }
+  if(!isEmpty(updateParam.addr)){
+    sql += ` addr = ?,`;
+    values.push(updateParam.addr);
+  }
+  // 网店地址
+  if(!isEmpty(updateParam.shop_addr)){
+    sql += ` shop_addr = ?,`;
+    values.push(updateParam.shop_addr);
+  }
+  if(!isEmpty(updateParam.tel)){
+    sql += ` tel = ?,`;
+    values.push(updateParam.tel);
+  }
+  if(!isEmpty(updateParam.fax)){
+    sql += ` fax = ?,`;
+    values.push(updateParam.fax);
+  }
+  if(!isEmpty(updateParam.email)){
+    sql += ` email = ?,`;
+    values.push(updateParam.email);
+  }
+  if(values.length === 0){
+    return Promise.reject('没有修改任何数据')
+  }
+  // 尝试移除最后一个逗号
+  sql = sql.substring(0,sql.length - 1);
+  sql += ` where id = ? limit 1`
+  values.push(baseId);
+  return mysql.pq(sql,values);
+}
+
+function addBaseInfo(data){
+  let sql = `insert into hfy_basic (wx_qrc, addr, tel, fax, email, shop_addr)
+                    values (?,?,?,?,?)`;
+  let {wx_qrc, addr, tel, fax, email, shop_addr} = data;
+  shop_addr = shop_addr || '';
+  return mysql.pq(sql,[wx_qrc,addr,tel,fax,email, shop_addr]);
+}
 
 
 module.exports = {
@@ -157,5 +215,7 @@ module.exports = {
   loadFiles,
   getFileById,
   deleteFile,
-
+  getBaseInfo,
+  editBaseInfo,
+  addBaseInfo
 }

+ 7 - 2
server/database/d_news.js

@@ -52,7 +52,7 @@ function searchAllNewsMini(type='array',searchParam,sort,page,limit){
 }
 
 function getNewsById(id){
-  let sql = `SELECT * FROM hfy_news WHERE id = ? limit 1`;
+  let sql = `SELECT news.*, n_type.type_key as type_key FROM hfy_news as news, hfy_news_type as n_type WHERE news.type_id = n_type.type_id and id = ? limit 1`;
   return mysql.pq(sql,[id]);
 }
 
@@ -114,6 +114,10 @@ function editArticleType(id, typeChange) {
     sql += `,seo_key = ?`;
     values.push(typeChange.seo_key);
   }
+  if(typeChange.sub_text){
+    sql += `,sub_text = ?`;
+    values.push(typeChange.sub_text);
+  }
   sql += ` WHERE type_id = ?`;
   values.push(id);
   return mysql.pq(sql, values);
@@ -129,7 +133,7 @@ function deleteArticleType(id) {
 function addArticleType(type, parentId) {
   let sql =
     `INSERT INTO hfy_news_type (
-date_time, type_name, type_key, type_sort, type_logo, seo_key, parent_id)
+date_time, type_name, type_key, type_sort, type_logo, seo_key, sub_text,parent_id)
 VALUES (?, ?, ?, ?, ?, ?, ?)`;
   let values = [];
   values.push(time.getUnixTimeStamp());
@@ -138,6 +142,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?)`;
   values.push(type.type_sort);
   values.push(type.type_logo);
   values.push(type.seo_key);
+  values.push(type.sub_text);
   values.push(parentId);
 
   return mysql.pq(sql,values);

+ 100 - 3
server/database/d_product.js

@@ -15,8 +15,10 @@ function loadProducts(key, page, limit) {
             p.type_id = p_type.type_id
             and p_type.type_key = ?`;
   values = [key];
+  sql += ` ORDER BY p.sort DESC, p.add_time DESC `;
   let _limitSql = limitSql(limit,page);
   sql += _limitSql.sql;
+  // 添加排序
   values.push(..._limitSql.values);
   return mysql.pq(sql, values);
 }
@@ -36,6 +38,86 @@ function getProductInfo(id) {
   values = [id];
   return mysql.pq(sql, values);
 }
+function addProduct(data, type_id) {
+  let sql = ``;
+  let values = [];
+  sql += `INSERT INTO hfy_product (
+            type_id,
+            name,
+            remark,
+            sort,
+            seo_key,
+            image,
+            sub_img,
+            detail,
+            overview,
+            parameter,
+            add_time) VALUES
+            (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
+  values.push(type_id);
+  values.push(data.name);
+  values.push(data.remark);
+  values.push(data.sort);
+  values.push(data.seo_key);
+  values.push(data.image);
+  values.push(data.sub_img);
+  values.push(data.detail);
+  values.push(data.overview);
+  values.push(data.parameter);
+  values.push(time.getUnixTimeStamp());
+  return mysql.pq(sql, values);
+}
+
+function editProduct(data, type_id, id) {
+  let sql = ``;
+  let values = [];
+  sql += `UPDATE hfy_product SET
+            type_id = ?,
+            name = ?,
+            remark = ?,
+            sort = ?,
+            seo_key = ?,
+            image = ?,
+            sub_img = ?,
+            detail = ?,
+            overview = ?,
+            parameter = ?
+          WHERE
+            proid = ?`;
+  values.push(type_id);
+  values.push(data.name);
+  values.push(data.remark);
+  values.push(data.sort);
+  values.push(data.seo_key);
+  values.push(data.image);
+  values.push(data.sub_img);
+  values.push(data.detail);
+  values.push(data.overview);
+  values.push(data.parameter);
+  values.push(id);
+  return mysql.pq(sql, values);
+}
+
+function deleteProduct(id) {
+  let sql = ``;
+  let values = [];
+  sql += `DELETE FROM hfy_product WHERE proid = ? limit 1`;
+  values = [id];
+  return mysql.pq(sql, values);
+}
+
+function getSubImages(id) {
+  let sql = ``;
+  let values = [];
+  sql += `SELECT
+              id, path
+          FROM
+            hfy_product_images
+          WHERE
+            proid = ?`;
+  values = [id];
+  return mysql.pq(sql, values);
+}
 
 function searchProducts(type='array',searchParam,sort,page,limit){
   let sql = ``;
@@ -64,7 +146,8 @@ function searchProducts(type='array',searchParam,sort,page,limit){
     sql += ` and p_type.type_key = ?`
     values.push(searchParam.type)
   }
-  return searchSql(mysql.pq,type,sql,values,limit,page);
+  sql += ` ORDER BY p.sort DESC, p.add_time DESC `;
+  return searchSql(mysql.pq, type, sql, values, limit, page);
 }
 
 /**
@@ -100,11 +183,12 @@ function searchProductsByMini(type='array',searchParam,sort,page,limit){
     sql += ` and p_type.type_key = ?`
     values.push(searchParam.type)
   }
+  sql += ` ORDER BY p.sort DESC, p.add_time DESC `;
   return searchSql(mysql.pq,type,sql,values,limit,page);
 }
 
 function loadTypes() {
-  let sql = `SELECT * FROM hfy_product_type`;
+  let sql = `SELECT * FROM hfy_product_type ORDER BY type_sort DESC`;
   return mysql.pq(sql, []);
 }
 
@@ -138,6 +222,10 @@ function editProductType(id, typeChange) {
     sql += `,type_key = ?`;
     values.push(typeChange.type_key);
   }
+  if(typeChange.sub_text){
+    sql += `,sub_text = ?`;
+    values.push(typeChange.sub_text);
+  }
   if(typeChange.type_sort){
     sql += `,type_sort = ?`;
     values.push(typeChange.type_sort);
@@ -146,6 +234,10 @@ function editProductType(id, typeChange) {
     sql += `,type_logo = ?`;
     values.push(typeChange.type_logo);
   }
+  if(typeChange.shop_addr){
+    sql += `,shop_addr = ?`;
+    values.push(typeChange.shop_addr);
+  }
   if(typeChange.seo_key){
     sql += `,seo_key = ?`;
     values.push(typeChange.seo_key);
@@ -163,7 +255,7 @@ function deleteProductType(id) {
 // 新增产品类型
 function addProductType(type) {
   let sql = `INSERT INTO hfy_product_type
-(date_time, type_name, type_key, type_sort, type_logo, seo_key)
+(date_time, type_name, type_key, type_sort, type_logo, seo_key, sub_text)
 VALUES (?, ?, ?, ?, ?, ?)`;
   let values = [];
   values.push(time.getUnixTimeStamp());
@@ -172,6 +264,7 @@ VALUES (?, ?, ?, ?, ?, ?)`;
   values.push(type.type_sort);
   values.push(type.type_logo);
   values.push(type.seo_key);
+  values.push(type.sub_text);
   return mysql.pq(sql, values);
 }
 
@@ -184,6 +277,10 @@ function getTypeByKey(key) {
 module.exports = {
   loadProducts,
   getProductInfo,
+  addProduct,
+  editProduct,
+  deleteProduct,
+  getSubImages,
   searchProducts,
   searchProductsByMini,
   loadTypes,

+ 8 - 3
server/database/d_solution.js

@@ -11,7 +11,8 @@ function loadSolution(key, page, limit) {
             news.title as name,
             news.image,
             news.source_val as source,
-            news.sourceType
+            news.sourceType,
+            n_type.type_key
           FROM
             hfy_news as news ,
             hfy_news_type as n_type
@@ -102,10 +103,14 @@ function getSolutionInfo(id) {
   let sql = ``;
   let values = [];
   sql += `SELECT
-            news.*
+            news.*,
+            n_type.type_key
           FROM
-            hfy_news as news
+            hfy_news as news,
+            hfy_news_type as n_type
           WHERE
+            news.type_id = n_type.type_id
+            and
             news.id = ?`;
   values = [id];
   return mysql.pq(sql, values);

+ 1 - 0
server/index.js

@@ -29,6 +29,7 @@ app.use(
   })
 );
 
+
 app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
 app.use(bodyParser.json({ limit: '10mb' }));
 

+ 36 - 0
server/router/r_base.js

@@ -193,4 +193,40 @@ router.get('/base',async (req,res)=>{
   }
 });
 
+router.get('/info', async(req, res)=>{
+  try{
+    log.info(`[平台基础信息获取] 获取基础数据信息,前后端通用数据`);
+    let err, data;
+    // 从数据库中读取数据.
+    [err, data] = await c.getBaseInfo();
+    if(err){
+      log.warn(`[平台基础信息获取] 获取基础数据 ${err.eMsg||err.message}`);
+      return controlError(res, err,`获取基础数据信息失败 ${err.eMsg||err.message}`);
+    }
+    return success(res, data);
+  }catch (e){
+    console.log(e);
+    ServerError(res, e, `获取基础数据异常 ${e.message}`);
+  }
+})
+
+router.post('/edit', checkLogin(progressField.session_hfy),
+  async (req, res) => {
+    try{
+      log.info(`[平台基础信息编辑] 修改平台基础信息`);
+      let body = req.body;
+      let err, data;
+      // 从数据库中读取数据.
+      [err, data] = await c.editBaseInfo(body);
+      if(err){
+        log.warn(`[平台基础信息编辑] 修改平台信息失败 ${err.eMsg||err.message}`);
+        return controlError(res, err,`修改平台信息 ${err.eMsg||err.message}`);
+      }
+      return success(res, data);
+    }catch (e){
+      console.log(e);
+      ServerError(res, e, `获取基础数据异常 ${e.message}`);
+    }
+});
+
 module.exports =  router;

+ 63 - 1
server/router/r_product.js

@@ -5,6 +5,7 @@ const log = require("../logger").logger("r_product","info")
 const typeTool = require('../tools/typeTool_cjs');
 const checkLogin = require("../middleware/checkSession");
 const progressField = require("../map/progressField");
+const codeMap = require("../map/rcodeMap");
 
 /**
  * 加载产品列表,根据类型
@@ -151,7 +152,8 @@ router.delete('/type/del', checkLogin(progressField.session_hfy), async (req, re
 router.get('/:id', async (req, res) => {
   try{
     let err, result;
-    let {id} = req.params;
+    let {id, lang} = req.params;
+
     if(!id){
       paramFail(res, "id is required");
       return;
@@ -168,4 +170,64 @@ router.get('/:id', async (req, res) => {
 
 
 
+router.post('/edit', checkLogin(progressField.session_hfy), async (req, res) => {
+  try{
+    let err, result;
+    const body = req.body;
+    let sub_images = body.sub_images;
+
+    if(!sub_images){
+      log.error(`无法获取产品子图`)
+      paramFail(res, "sub_images is required");
+    }
+
+    log.info(`[编辑产品] body=${JSON.stringify(body)}`);
+    [err, result] = await c.editProduct(body);
+    if(err){ return controlError(res, err, null);}
+    success(res, result);
+  }catch (e) {
+    ServerError(res, null, e.message);
+  }
+})
+
+router.post('/add', checkLogin(progressField.session_hfy), async (req, res) => {
+  try{
+    let err, result;
+    const body = req.body;
+    let sub_images = body.sub_images;
+    if(!sub_images){
+      log.error(`无法获取产品子图:${JSON.stringify(body)}`)
+      return [
+        {
+          eCode: codeMap.NotParam,
+          eMsg: `无法获取产品图`
+        }, null]
+    }
+    log.info(`[编辑产品] body=${JSON.stringify(body)}`);
+    [err, result] = await c.addProduct(body);
+    if(err){ return controlError(res, err, null);}
+    success(res, result);
+  }catch (e) {
+    ServerError(res, null, e.message);
+  }
+})
+
+router.post('/del', checkLogin(progressField.session_hfy), async (req, res) => {
+  try{
+    let err, result;
+    const id = req.query.id;
+    log.info(`[移除产品] id=${id} `);
+    if(!id ){
+      paramFail(res, "id is must be two ");
+      return;
+    }
+    [err, result] = await c.deleteProduct(id);
+    if(err){ return controlError(res, err, null);}
+    success(res, result);
+  }catch (e) {
+    ServerError(res, null, e.message);
+  }
+})
+
+
 module.exports =  router ;

+ 1 - 1
server/tools/searchSql.js

@@ -61,7 +61,7 @@ async function searchHandle(errorText,dbFn,_params,_sort,page,limit ,
     return [null,result];
 }
 
-function searchSql (fn,type,sql,values,limit,page){
+function searchSql (fn,type,sql,values,limit,page, _sortKeys){
   // 添加分页
   if(type==='array'){
     let _limitSql = limitSql(limit,page);

+ 26 - 2
store/index.js

@@ -24,18 +24,22 @@ export const indexTypes = {
     setProductTypes: 'setProductTypes',
     setNewsTypes: 'setNewsTypes',
     setCarousel: 'setCarousel',
+    setPlatform: 'setPlatform'
   }
 }
 
 export const modules = {
   index: {
     state: {
+      // 平台基础信息
+      platform: {},
       // 产品类别
       pTypes: [],
       // 新闻类别
       nTypes: [],
       // 轮播数据
       carousel: [],
+
     },
     mutations: {
       [indexTypes.mutations.setProductTypes](state, pTypes){
@@ -46,6 +50,9 @@ export const modules = {
       },
       [indexTypes.mutations.setCarousel](state, carousel){
         state.carousel = carousel;
+      },
+      [indexTypes.mutations.setPlatform](state, platform){
+        state.platform = platform;
       }
     },
     actions : {
@@ -62,6 +69,7 @@ export const modules = {
           let result = res.data;
           if(result.code !== rCode.OK){return console.log(result.msg);}
           let data = result.data;
+          commit(indexTypes.mutations.setPlatform, data.baseInfo);
           commit(indexTypes.mutations.setProductTypes, data.pType);
           commit(indexTypes.mutations.setNewsTypes, data.nType);
           commit(indexTypes.mutations.setCarousel, data.carousel);
@@ -93,12 +101,28 @@ export const modules = {
         console.log(state.nTypes);
         let arr = state.nTypes.filter(item =>
           item.parent_type === dbField_esm.db_base.newsType.solution);
-        return _transform(arr);
+        let res = _transform(arr);
+        res.unshift({
+          en_text : "全部类型",
+          icon : "/image/all.svg",
+          text : "全部类型",
+          key : "all",
+          typeKey : "all",
+        })
+        return res;
       },
       newsTypes: state => {
         let arr = state.nTypes.filter(item =>
           item.parent_type === dbField_esm.db_base.newsType.news);
-        return _transform(arr);
+        let res = _transform(arr);
+        res.unshift({
+          en_text : "全部类型",
+          icon : "/image/all.svg",
+          text : "全部类型",
+          key : "all",
+          typeKey : "all",
+        })
+        return res;
       },
 
     }

+ 5 - 1
until/form/rules.js

@@ -213,7 +213,11 @@ export const paramsRules = [
   {
     checkFields: ['tags', 'seo_tags'],
     rules: [tagsRule]
-  }
+  },
+  {
+    checkFields: ['href', 'url'],
+    rules: hrefRule
+  },
 ]
 
 

+ 7 - 1
until/formTool.js

@@ -6,7 +6,13 @@ export function initForm(formObject){
   for(let i = 0; i < keys.length; i++){
     let key = keys[i];
     if(formObject[key].options && formObject[key].options.length > 0){
-      formObject[key].init = formObject[key].options[0].key;
+      let initVal = formObject[key].init;
+      let option = formObject[key].options.find(item => item.key === initVal);
+      if (option) {
+        formObject[key].init = option.key;
+      }else{
+        formObject[key].init = formObject[key].options[0].key;
+      }
     }
     formObject[key].val = formObject[key].init;
     formObject[key].msg = '';

+ 1 - 0
until/time.js

@@ -4,6 +4,7 @@
  * @returns {string}
  */
 export function timestampToTime(timestamp) {
+  if (!timestamp) return '';
   timestamp = timestamp.toString().length === 10 ? timestamp * 1000 : timestamp;
   let date = new Date(timestamp); //时间戳为10位需*1000,时间戳为13位的话不需乘1000
   let Y = date.getFullYear() + '-';

+ 2 - 0
until/unescapeHtml.js

@@ -15,6 +15,7 @@ export function unescape(html) {
  * @returns {*}
  */
 export function unescapeHtml(html) {
+  if(!html) return html;
   return html
     .replace(html ? /&(?!#?\w+;)/g : /&/g, '')
     .replace(/&lt;/g, "<")
@@ -32,6 +33,7 @@ export function unescapeHtml(html) {
  * @returns {*}
  */
 export function escapeHtml(html) {
+  if(!html) return html;
   return html
     .replace(/&/g, '&amp;')
     .replace(/</g, '&lt;')

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