Browse Source

2025-2-20

cmy 9 months ago
parent
commit
8eece1955d

+ 1 - 1
.env.base

@@ -8,7 +8,7 @@ VITE_API_BASE_PATH=
 VITE_BASE_PATH=/
 
 # 标题
-VITE_APP_TITLE=ElementAdmin
+VITE_APP_TITLE=七牛数字
 
 # 是否全量引入element-plus样式
 VITE_USE_ALL_ELEMENT_PLUS_STYLE=true

+ 1 - 1
.env.gitee

@@ -20,7 +20,7 @@ VITE_SOURCEMAP=false
 VITE_OUT_DIR=dist-pro
 
 # 标题
-VITE_APP_TITLE=ElementAdmin
+VITE_APP_TITLE=七牛数字
 
 # 是否包分析
 VITE_USE_BUNDLE_ANALYZER=false

+ 1 - 1
.env.test

@@ -20,7 +20,7 @@ VITE_SOURCEMAP=true
 VITE_OUT_DIR=dist-test
 
 # 标题
-VITE_APP_TITLE=ElementAdmin
+VITE_APP_TITLE=七牛数字
 
 # 是否包分析
 VITE_USE_BUNDLE_ANALYZER=false

+ 52 - 1
src/api/goods/index.ts

@@ -1,6 +1,14 @@
 import request from '@/axios'
 import { REQUEST_BASE } from '@/constants'
-import { categoryData, goodsData, goodsSearch, auditData } from './types'
+import {
+  categoryData,
+  goodsData,
+  goodsSearch,
+  auditData,
+  searchProductRule,
+  productRule,
+  productRuleValue
+} from './types'
 import { productDataParse } from './parseData'
 export const getProductCategory = (params: {
   page?: number
@@ -81,3 +89,46 @@ export const getProductDetail = (
     url: `${REQUEST_BASE}/product/read/${id}`
   })
 }
+
+/**
+ * 获取产品规格信息
+ *
+ * 通过发送GET请求来获取指定名称的产品规格信息这个函数级别的注释解释了函数的目的和工作方式
+ *
+ * @param name 产品规格的名称,用于查询特定的产品规格
+ * @returns 返回一个Promise,解析为包含响应数据的IResponse对象
+ */
+export const getProductRule = (params: searchProductRule): Promise<IResponse> => {
+  return request.get({
+    url: `${REQUEST_BASE}/product_rule/index`,
+    params
+  })
+}
+export const addProductRule = (data: productRule): Promise<IResponse> => {
+  return request.post({
+    url: `${REQUEST_BASE}/product_rule/save`,
+    data
+  })
+}
+export const putProductRule = (data: productRule): Promise<IResponse> => {
+  return request.put({
+    url: `${REQUEST_BASE}/product_rule/update/${data.id}`,
+    data
+  })
+}
+export const delProductRule = (id: number): Promise<IResponse> => {
+  return request.delete({
+    url: `${REQUEST_BASE}/product_rule/delete/${id}`
+  })
+}
+export const getProductRuleDetail = (id: number): Promise<IResponse> => {
+  return request.get({
+    url: `${REQUEST_BASE}/product_rule/read/${id}`
+  })
+}
+export const getAttrSelectList = (id: number, attrs: productRuleValue[]): Promise<IResponse> => {
+  return request.post({
+    url: `${REQUEST_BASE}/product/is_format_attr/${id}`,
+    data: { attrs }
+  })
+}

+ 29 - 0
src/api/goods/types.ts

@@ -56,3 +56,32 @@ export interface auditData {
   is_verify: -1 | 1 //审核状态:-1=拒绝,1=通过
   refusal: string //未通过原因
 }
+
+export interface searchProductRule {
+  rule_name?: string
+  page?: number
+  limit?: number
+}
+export interface productRuleValue {
+  attrHidden: '' | true | false
+  detail: string[]
+  detailValue: string
+  value: string
+}
+export interface productRule {
+  id: number
+  rule_name: string
+  rule_value: productRuleValue[]
+}
+export interface AttrBaseItem {
+  bar_code: string
+  cost: number
+  detail: any
+  integral: number
+  ot_price: number
+  pic: string[]
+  price: number
+  stock: number
+  weight: number
+  volume: number
+}

+ 22 - 5
src/components/UpFile/src/index.vue

@@ -31,6 +31,7 @@ import { FormSchema } from '@/components/Form'
 import type Node from 'element-plus/es/components/tree/src/model/node'
 import upFile from './components/upFile.vue'
 import { useIcon } from '@/hooks/web/useIcon'
+import { createVideoViewer } from '@/components/VideoPlayer'
 const icon = useIcon({ icon: 'vi-ep:upload-filled' })
 const successicon = useIcon({ icon: 'vi-ep:select' })
 interface Tree {
@@ -330,14 +331,19 @@ const delData = async (id?: string) => {
               </div>
             </template>
           </ElImage>
-          <video
-            preload="none"
+          <div
             v-if="fileType == 2"
-            controls
             class="card-list-img"
-            :src="item.satt_dir"
+            @click="
+              createVideoViewer({
+                url: item.satt_dir
+              })
+            "
           >
-          </video>
+            <div class="payBackground flex items-center justify-center">
+              <Icon icon="vi-ep:caret-right" :size="80" />
+            </div>
+          </div>
           <template #footer>
             <BaseButton
               class="buttom-checked"
@@ -407,6 +413,17 @@ const delData = async (id?: string) => {
   .card-list-img {
     width: 100%;
     height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    text-align: center;
+    .payBackground {
+      color: #fff;
+      width: 90px;
+      height: 90px;
+      background-color: rgba(0, 0, 0, 0.2);
+      border-radius: 100%;
+    }
   }
   .buttom-checked {
     width: 74px;

+ 12 - 2
src/components/UpFile/src/upImageButtom.vue

@@ -1,6 +1,6 @@
 <script setup lang="tsx">
 import upFile from './index.vue'
-import { PropType, ref, defineModel, watch } from 'vue'
+import { PropType, ref, defineModel, watch, nextTick } from 'vue'
 import { Dialog } from '@/components/Dialog'
 import { ElImage, ElMessage } from 'element-plus'
 const modelValue = defineModel<string[]>({
@@ -22,6 +22,7 @@ const saveLoading = ref(false)
 const saveType = ref('add')
 const upImageIndex = ref(0)
 const editImage = (index: number, type) => {
+  console.log(index, type)
   if (type === 'del') {
     modelValue.value.splice(index, 1)
     modelValue.value = [...modelValue.value]
@@ -33,7 +34,7 @@ const editImage = (index: number, type) => {
     showImage.value = true
   }
 }
-const emit = defineEmits(['confirm'])
+const emit = defineEmits(['change'])
 const save = async () => {
   saveLoading.value = true
   if (saveType.value === 'add') {
@@ -44,6 +45,9 @@ const save = async () => {
       return ElMessage.error('最多选择再' + props.num + '张图片')
     }
     modelValue.value = [...modelValue.value, ...actionData.value.map((item: any) => item.att_dir)]
+    await nextTick()
+    console.log(modelValue.value, 'modelValue.value')
+    emit('change', modelValue.value)
   } else if (saveType.value === 'edit') {
     if (actionData.value.length > 1) {
       saveLoading.value = false
@@ -51,6 +55,9 @@ const save = async () => {
     }
     modelValue.value.splice(upImageIndex.value, 1, actionData.value[0].att_dir)
     modelValue.value = [...modelValue.value]
+    await nextTick()
+    console.log(modelValue.value, 'modelValue.value')
+    emit('change', modelValue.value)
   }
 
   actionData.value = []
@@ -87,6 +94,9 @@ const handleDragEnter = (e, item) => {
   newItems.splice(dst, 0, ...newItems.splice(src, 1))
   modelValue.value = newItems
 }
+defineExpose({
+  editImage
+})
 </script>
 
 <template>

+ 43 - 14
src/components/tableImage/tableImage.vue

@@ -3,7 +3,7 @@ import { PropType } from 'vue'
 import { ElImage } from 'element-plus'
 import { Icon } from '@/components/Icon'
 import { createImageViewer } from '@/components/ImageViewer'
-defineProps({
+const props = defineProps({
   src: {
     type: String,
     default: ''
@@ -19,24 +19,53 @@ defineProps({
   ind: {
     type: Number,
     default: 0
+  },
+  isShowList: {
+    type: Boolean,
+    default: false
+  },
+  isShowImageViewer: {
+    type: Boolean,
+    default: true
   }
 })
+const showImageList = (src, ind) => {
+  console.log(props.isShowImageViewer, 'props.isShowImageViewer')
+  if (props.isShowImageViewer) {
+    if (src) {
+      createImageViewer({
+        urlList: props.list.length > 0 ? props.list : [src],
+        initialIndex: !isNaN(ind) ? ind : props.ind || 0,
+        hideOnClickModal: true
+      })
+    }
+  } else {
+    emit('click')
+  }
+}
+const emit = defineEmits(['click'])
 </script>
 
 <template>
-  <div
-    class="lh-none"
-    @click="
-      src
-        ? createImageViewer({
-            urlList: list.length > 0 ? list : [src],
-            initialIndex: ind || 0,
-            hideOnClickModal: true
-          })
-        : null
-    "
-  >
-    <ElImage :alt="alt" fit="contain" class="w-[50px] h-[50px]" :src="src">
+  <div class="lh-none" @click.stop="showImageList(src, undefined)">
+    <ElImage v-if="!isShowList" :alt="alt" fit="contain" class="w-[50px] h-[50px]" :src="src">
+      <template #error>
+        <div
+          class="flex items-center justify-center w-full h-full bg-gray-200 border-rd-[4px] overflow-hidden"
+        >
+          <Icon icon="vi-ep:picture" />
+        </div>
+      </template>
+    </ElImage>
+    <ElImage
+      v-else
+      :alt="alt"
+      fit="contain"
+      class="w-[50px] h-[50px]"
+      v-for="(item, index) in list"
+      :key="index"
+      :src="item"
+    >
       <template #error>
         <div
           class="flex items-center justify-center w-full h-full bg-gray-200 border-rd-[4px] overflow-hidden"

+ 2 - 62
src/locales/zh-CN.ts

@@ -105,7 +105,7 @@ export default {
     small: '小'
   },
   login: {
-    welcome: '欢迎使用本系统',
+    welcome: '欢迎使用七牛数字后台管理',
     message: '开箱即用的中后台管理系统',
     username: '用户名',
     password: '密码',
@@ -124,69 +124,9 @@ export default {
   },
   router: {
     login: '登录',
-    level: '多级菜单',
-    menu: '菜单',
-    menu1: '菜单1',
-    menu11: '菜单1-1',
-    menu111: '菜单1-1-1',
-    menu12: '菜单1-2',
-    menu2: '菜单2',
     dashboard: '首页',
     analysis: '分析页',
-    workplace: '工作台',
-    guide: '引导',
-    component: '组件',
-    icon: '图标',
-    echart: '图表',
-    countTo: '数字动画',
-    watermark: '水印',
-    qrcode: '二维码',
-    highlight: '高亮',
-    infotip: '信息提示',
-    form: '表单',
-    defaultForm: '全部示例',
-    search: '查询',
-    table: '表格',
-    defaultTable: '基础示例',
-    editor: '编辑器',
-    richText: '富文本',
-    jsonEditor: 'JSON编辑器',
-    codeEditor: '代码编辑器',
-    dialog: '弹窗',
-    imageViewer: '图片预览',
-    descriptions: '描述',
-    example: '综合示例',
-    exampleDialog: '综合示例 - 弹窗',
-    examplePage: '综合示例 - 页面',
-    exampleAdd: '综合示例 - 新增',
-    exampleEdit: '综合示例 - 编辑',
-    exampleDetail: '综合示例 - 详情',
-    errorPage: '错误页面',
-    authorization: '权限管理',
-    user: '用户管理',
-    role: '角色管理',
-    document: '文档',
-    inputPassword: '密码输入框',
-    sticky: '黏性',
-    treeTable: '树形表格',
-    PicturePreview: '表格图片预览',
-    department: '部门管理',
-    menuManagement: '菜单管理',
-    permission: '权限测试页',
-    function: '功能',
-    multipleTabs: '多开标签页',
-    details: '详情页',
-    iconPicker: '图标选择器',
-    request: '请求',
-    waterfall: '瀑布流',
-    imageCropping: '图片裁剪',
-    videoPlayer: '视频播放器',
-    tableVideoPreview: '表格视频预览',
-    cardTable: '卡片表格',
-    personalCenter: '个人中心',
-    personal: '个人',
-    avatars: '头像列表',
-    iAgree: '我同意'
+    personalCenter: '个人中心'
   },
   permission: {
     hasPermission: '请设置操作权限值'

+ 8 - 0
src/router/model/Goods.ts

@@ -34,6 +34,14 @@ export default {
       meta: {
         title: '添加修改商品'
       }
+    },
+    {
+      path: 'rule',
+      component: () => import('@/views/Goods/rule/index.vue'),
+      name: `${pre}-rule`,
+      meta: {
+        title: '商品规格'
+      }
     }
   ]
 }

+ 537 - 23
src/views/Goods/edit/add.vue

@@ -1,5 +1,5 @@
 <script setup lang="tsx">
-import { onMounted, reactive, ref } from 'vue'
+import { onMounted, reactive, ref, useTemplateRef, unref } from 'vue'
 import { ContentWrap } from '@/components/ContentWrap'
 import {
   ElInput,
@@ -14,15 +14,30 @@ import {
   ElMessage,
   ElMessageBox,
   ElSelect,
-  ElOption
+  ElOption,
+  ElRadioGroup,
+  ElRadioButton,
+  ElTag,
+  ElTableColumn,
+  ElTable,
+  ElDivider
 } from 'element-plus'
 import { Editor, EditorExpose } from '@/components/Editor'
 import { useRoute, useRouter } from 'vue-router'
 import { goodsData, attrsValue } from '@/api/goods/types'
-import { getProductCategory, addProduct, getProductDetail, putProduct } from '@/api/goods'
+import {
+  getProductCategory,
+  addProduct,
+  getProductDetail,
+  putProduct,
+  getAttrSelectList
+} from '@/api/goods'
+import { getProductRule } from '@/api/goods'
+import { productRule, productRuleValue, AttrBaseItem } from '@/api/goods/types'
 import { UpImgButtom } from '@/components/UpFile'
 import { getStoreList } from '@/api/store'
 import { isArray } from 'lodash-es'
+import { TableImage } from '@/components/tableImage'
 const pageTitle = ref('添加')
 const { params } = useRoute()
 const { push } = useRouter()
@@ -62,22 +77,48 @@ const attrs_value = reactive<
   ot_price: 0,
   cost: 0
 })
+
+/**
+ * 创建并返回一个字段验证规则对象
+ *
+ * @param {string} field - 需要验证的字段名称
+ * @param {string} errorMessage - 当验证失败时显示的错误信息
+ * @returns {Object} 包含验证规则的对象,包括错误信息、是否必填、及自定义验证器
+ */
 const validateField = (field, errorMessage) => {
+  // 返回一个对象,定义了验证规则
   return {
+    // 错误信息,在验证失败时展示
     message: errorMessage,
+    // 标记字段为必填
     required: true,
+    // 自定义验证器,接收三个参数:规则、数据、及回调函数
     validator: (_, __, callback) => {
+      // 检查指定字段在属性值对象中是否存在
       if (!attrs_value[field]) {
+        // 如果字段不存在,通过回调函数返回一个错误对象,包含错误信息
         callback(new Error(errorMessage))
       } else {
+        // 如果字段存在,调用回调函数但不传递错误,表示验证通过
         callback()
       }
     }
   }
 }
 
+/**
+ * 根据规格类型获取对应的验证规则
+ *
+ * 此函数用于根据不同的商品规格类型返回相应的验证规则它接受一个规格类型参数(specType),
+ * 并根据此参数的值返回不同的验证规则对象如果specType的值是0,则返回包含商品图片、库存、售价和成本价的验证规则;
+ * 如果specType的值是1,则返回一个空对象表示没有验证规则;如果specType的值不是0或1,则在控制台输出警告信息并返回一个空对象
+ *
+ * @param {number} specType - 商品的规格类型,用于确定返回哪种验证规则
+ * @returns {Object} - 返回对应规格类型的验证规则对象
+ */
 const getRulesForSpecType = (specType) => {
   if (specType === 0) {
+    // 当规格类型为0时,返回包含商品图片、库存、售价和成本价验证规则的对象
     return {
       sutimage: [validateField('image', '请选择商品图片')],
       stock: [validateField('stock', '请填写库存')],
@@ -85,8 +126,10 @@ const getRulesForSpecType = (specType) => {
       cost: [validateField('cost', '请填写成本价')]
     }
   } else if (specType === 1) {
+    // 当规格类型为1时,返回一个空对象表示没有验证规则
     return {}
   } else {
+    // 当规格类型不是0或1时,输出警告信息并返回一个空对象
     console.warn('Unknown spec_type:', specType)
     return {}
   }
@@ -116,7 +159,7 @@ const rules = reactive({
   ...getRulesForSpecType(formData.spec_type)
 })
 const formRef = ref<FormInstance>()
-
+// 分类查询功能
 const cateIdsProps = {
   checkStrictly: false,
   // emitPath: false,
@@ -143,39 +186,71 @@ const cateIdsProps = {
 }
 
 const editorRef = ref<typeof Editor & EditorExpose>()
+/**
+ * 递归地展平类别ID数组
+ * 此函数的目的是将一个包含数字和数字数组的数组转换成一个不重复的数字集合
+ * 这在处理商品分类ID时特别有用,其中子分类ID需要被展平以便于后续处理
+ *
+ * @param items - 一个数组,其元素可以是数字或数字数组,代表类别ID
+ * @returns 返回一个Set,包含所有展平后的类别ID,不包含重复项
+ */
 const flattenCateIds = (items: (number | number[])[]): Set<number> => {
+  // 初始化一个空的Set集合,用于存储最终展平的类别ID
   const result = new Set<number>()
 
+  /**
+   * 递归函数,用于展平类别ID
+   * 如果当前项是一个数组,则遍历数组的每一项并递归调用flatten
+   * 如果当前项是一个数字,则将其添加到result集合中
+   *
+   * @param item - 当前要处理的类别ID或类别ID数组
+   */
   const flatten = (item: number | number[]) => {
+    // 判断当前项是否为数组,如果是,则递归处理数组中的每一项
     if (isArray(item)) {
       item.forEach(flatten)
     } else {
+      // 如果当前项不是数组,则将其添加到result集合中
       result.add(item)
     }
   }
 
+  // 遍历输入的items数组,对每一项调用flatten函数进行展平处理
   items.forEach(flatten)
 
+  // 返回包含所有展平后类别ID的Set集合
   return result
 }
+/**
+ * 保存表单数据
+ * 此函数在用户提交表单时被触发,它首先验证表单数据的有效性,如果有效则处理数据并调用相应的API进行保存
+ * @param {FormInstance | undefined} formEl - 表单实例,用于表单验证和获取表单数据
+ * @returns {Promise<Array>} - 返回一个空数组,当前函数没有实际返回值,但为了保持接口一致性,返回了空数组
+ */
 const save = async (formEl: FormInstance | undefined) => {
+  // 检查表单实例是否存在,如果不存在则返回空数组
   if (!formEl) return []
+
+  // 验证表单数据,如果验证失败,则显示错误消息并中断保存流程
   const valid = await formEl.validate((valid, fields) => {
     if (valid) {
       console.log('submit!')
     } else {
       for (const key in fields) {
-        // console.log(fields)
         ElMessage.error(fields[key][0].message)
         return
       }
       console.log('error submit!', fields)
     }
   })
-  // console.log(valid, 'params')
+
+  // 如果表单数据有效,则准备数据并调用API进行保存
   if (valid) {
+    // 将分类ID转换为数组形式,以便后续处理
     const cateIds: number[] = Array.from(flattenCateIds(formData.cate_ids)).map((re) => re)
     console.log(cateIds, 'cateIds')
+
+    // 构建要保存的商品数据对象
     const data: goodsData = {
       store_id: formData.store_id,
       name: formData.name,
@@ -184,7 +259,7 @@ const save = async (formEl: FormInstance | undefined) => {
       slider_image: formData.slider_image,
       info: formData.info,
       image: formData.image,
-      keyword: formData.keyword,
+      keyword: formData.keyword || [],
       cate_ids: cateIds,
       postage: formData.postage,
       temp_id: formData.temp_id,
@@ -198,18 +273,28 @@ const save = async (formEl: FormInstance | undefined) => {
       sort: formData.sort,
       is_show: formData.is_show
     }
+
+    // 根据商品规格类型调整attrs_value字段
     if (formData.spec_type == 0) {
       data.attrs_value = [{ ...attrs_value }]
     }
+    // 多规格商品处理
+    if (formData.spec_type == 1) {
+      data.attrs_value = productAttrList.value.value.map((res) => {
+        return res
+      })
+    }
     try {
       let res = {}
-      console.log(params.type == 'add', '11111')
+      // 根据操作类型(添加或编辑)调用相应的API
       if (params.type == 'add') {
         res = await addProduct(data)
       } else if (params.type == 'edit') {
         data.id = formData.id
         res = await putProduct(data)
       }
+
+      // 处理API调用结果,如果成功则显示成功消息并询问用户是否返回商品列表
       if (res) {
         let title = '添加'
         if (params.type == 'edit') {
@@ -231,9 +316,10 @@ const save = async (formEl: FormInstance | undefined) => {
     }
   }
 }
-
 const loadingData = ref(false)
 onMounted(async () => {
+  //加载商品规格
+  ruleList()
   if (params.type == 'add') {
     pageTitle.value = '添加商品'
   } else if (params.type == 'edit') {
@@ -248,7 +334,7 @@ onMounted(async () => {
       formData.video_link = data.video_link
       formData.slider_image = data.slider_image
       formData.image = data.image
-      formData.keyword = data.keyword.split(',')
+      formData.keyword = data.keyword ? data.keyword.split(',') : []
       formData.cate_ids = data.cate_ids.map((re) => parseInt(re))
       formData.postage = data.postage
       formData.temp_id = data.temp_id
@@ -272,6 +358,9 @@ onMounted(async () => {
         attrs_value.suk = values.suk
         attrs_value.image = [values.image]
       }
+      // if(formData.spec_type == 1){
+
+      // }
       getStore('', data.store_id)
     } catch (error) {
       console.log(error, 'error')
@@ -329,6 +418,219 @@ const getStore = async (query = '', id = '') => {
     storeLoading.value = false
   }
 }
+const selectRuleList = ref<productRule[]>([]) //可选规格列表
+const selectRuleIndex = ref() //选中的规格下标
+const actionAttrs = reactive<productRuleValue[]>([]) //选中的规格
+const addAttrShow = ref(false) //添加商品规格显示
+//新增规格输入框
+const attrItem = reactive({
+  name: '',
+  value: ''
+})
+const attrBase = reactive<AttrBaseItem[]>([
+  {
+    bar_code: '', //产品编号
+    cost: 0, //成本价
+    detail: {},
+    integral: 0, //积分
+    ot_price: 0, //原价
+    pic: [], //图片
+    price: 0, //售价
+    stock: 0, //库存
+    weight: 0, //重量
+    volume: 0 //体积
+  }
+])
+//商品多规格数据保存
+const productAttrList = ref<{
+  attr: any[]
+  header: any[]
+  value: any[]
+}>({
+  attr: [],
+  header: [],
+  value: []
+})
+/**
+ * 加载商品规格列表
+ */
+const ruleList = async () => {
+  const res = await getProductRule({ page: 1, limit: 100 })
+  selectRuleList.value = res.data
+}
+/**
+ * 选择规则
+ *
+ * @param ruleIndex 规则在列表中的索引,用于标识特定的规则
+ */
+const selectAttrs = (ruleIndex: number) => {
+  actionAttrs.splice(0, actionAttrs.length - 1)
+  // 将选中的规则属性赋值给操作属性对象,以便在界面上进行展示或进一步操作
+  actionAttrs.push(...selectRuleList.value[ruleIndex].rule_value)
+  // 打印操作属性对象到控制台,用于调试目的
+  // console.log(actionAttrs, 'actionItem')
+}
+
+/**
+ * 删除规则
+ *
+ * @param tag 规则对象
+ */
+const delAttrlist = (ind: number) => {
+  actionAttrs.splice(ind, 1)
+}
+/**
+ * 删除规则属性
+ *
+ * @param name 规则属性名称
+ * @param tag 规则属性对象
+ */
+const delAttrlistItem = (name: string, tag: productRuleValue) => {
+  tag.detail.splice(tag.detail.indexOf(name), 1)
+}
+/**
+ * todo: 添加规则属性
+ * @param tag 要添加的规格参数值
+ */
+const addAttrItem = (tag: productRuleValue) => {
+  if (tag.detail.indexOf(tag.detailValue) > -1) {
+    ElMessage.error('规格参数已存在')
+    return
+  }
+  if (tag.detailValue) {
+    tag.detail.push(tag.detailValue)
+  }
+  tag.attrHidden = ''
+  tag.detailValue = ''
+}
+/**
+ * 生成多规格填写列表
+ */
+const attrsConfirm = async () => {
+  const { data } = await getAttrSelectList(formData.id || 0, actionAttrs)
+  const attrs = {
+    attr: data.attr,
+    header: data.header,
+    value: data.value.map((re) => {
+      re.pic = re.pic ? [re.pic] : []
+      return re
+    })
+  }
+  productAttrList.value = attrs
+}
+
+/**
+ * 添加规则
+ */
+const addAttr = () => {
+  actionAttrs.push({
+    attrHidden: '',
+    detail: [attrItem.value],
+    detailValue: '',
+    value: attrItem.name
+  })
+  attrItem.value = ''
+  attrItem.name = ''
+  addAttrShow.value = false
+}
+/**
+ *
+ * @param index 删除的规格属性下标
+ */
+const delAttrItem = (index: number, arr: any[]) => {
+  arr.splice(index, 1)
+  console.log(arr, 'index', index)
+}
+/**
+ * 初始化批量设置规格
+ */
+const initAttrBase = () => {
+  attrBase[0] = {
+    bar_code: '', //产品编号
+    cost: 0, //成本价
+    detail: {},
+    integral: 0, //积分
+    ot_price: 0, //原价
+    pic: [], //图片
+    price: 0, //售价
+    stock: 0, //库存
+    weight: 0, //重量
+    volume: 0 //体积
+  }
+}
+/**
+ * 批量设置
+ */
+const batchSet = () => {
+  const value = productAttrList.value.value
+  for (let i = 0; i < value.length; i++) {
+    value[i].pic = attrBase[0].pic
+    value[i].bar_code = attrBase[0].bar_code
+    value[i].cost = attrBase[0].cost
+    value[i].integral = attrBase[0].integral
+    value[i].price = attrBase[0].price
+    value[i].ot_price = attrBase[0].ot_price
+    value[i].stock = attrBase[0].stock
+    value[i].weight = attrBase[0].weight
+    value[i].volume = attrBase[0].volume
+  }
+}
+const upImageButtomRef = useTemplateRef('upImageButtomRef')
+
+const selectPic = ref<string[]>([])
+const selectPicIndex = ref(0)
+const selectPicObj = ref('header')
+
+/**
+ * 点击上传图片回调函数
+ * @param {string[]} pic - 图片数组
+ * @param {number} index - 图片在数组中的索引
+ * @param {string} type - 图片类型
+ */
+const clickUpImage = (index: number, type: string) => {
+  // 设置当前选中图片的类型
+  selectPicObj.value = type
+  // 设置当前选中图片的索引
+  selectPicIndex.value = index
+  // 获取上传图片元素
+  const picElment = unref(upImageButtomRef)
+  // 设置当前操作的图片数组
+  selectPic.value = []
+  // 日志输出上传图片元素,用于调试
+  // console.log(picElment, 'picElment')
+  // 调用编辑图片方法,这里以添加图片为例
+  picElment?.editImage(0, 'add')
+}
+
+/**
+ * 根据不同的图片选择对象类型,更新相应的图片数组
+ * @param {string[]} pic - 要更新的图片数组
+ */
+const changePic = (pic: string[]) => {
+  console.log(pic, 'pic')
+  // 根据选中的图片对象类型,更新对应的图片数组
+  switch (selectPicObj.value) {
+    case 'header':
+      // 更新产品属性列表中的图片数组
+      productAttrList.value.value[selectPicIndex.value].pic = pic
+      break
+    case 'attrBase':
+      // 更新基础属性中的图片数组
+      attrBase[0].pic = pic
+      break
+    case 'banner':
+      // 更新表单数据中的图片数组
+      formData.image = pic
+      break
+    case 'attrOne':
+      // 更新单规格的图片数组
+      attrs_value.image = pic
+      break
+    default:
+      // 其他情况不做处理
+      break
+  }
+}
 </script>
 <template>
   <ContentWrap
@@ -395,7 +697,11 @@ const getStore = async (query = '', id = '') => {
             />
           </ElFormItem>
           <ElFormItem label="商品封面图" prop="image">
-            <UpImgButtom v-model="formData.image" />
+            <TableImage
+              :isShowImageViewer="false"
+              :src="formData.image[0]"
+              @click="clickUpImage(0, 'banner')"
+            />
           </ElFormItem>
           <ElFormItem label="商品轮播图" prop="slider_image">
             <UpImgButtom
@@ -422,9 +728,19 @@ const getStore = async (query = '', id = '') => {
           </ElFormItem>
         </ElTabPane>
         <ElTabPane label="商品规格" :name="2">
+          <ElFormItem label="规格类型" prop="spec_type">
+            <ElRadioGroup v-model="formData.spec_type">
+              <ElRadioButton label="单规格" :value="0" />
+              <ElRadioButton label="多规格" :value="1" />
+            </ElRadioGroup>
+          </ElFormItem>
           <template v-if="formData.spec_type == 0">
             <ElFormItem label="商品规格图" prop="sutimage">
-              <UpImgButtom v-model="attrs_value.image" />
+              <TableImage
+                :isShowImageViewer="false"
+                :src="attrs_value.image[0]"
+                @click="clickUpImage(0, 'attrOne')"
+              />
             </ElFormItem>
             <ElFormItem label="售价" prop="price">
               <ElInput type="number" v-model="attrs_value.price" placeholder="请输入商品单位" />
@@ -439,18 +755,210 @@ const getStore = async (query = '', id = '') => {
               <ElInput type="number" v-model="attrs_value.stock" placeholder="请输入商品单位" />
             </ElFormItem>
           </template>
+          <template v-if="formData.spec_type == 1">
+            <ElFormItem label="选择规格" prop="spec_type">
+              <div class="w-full">
+                <ElSelect
+                  v-model="selectRuleIndex"
+                  placeholder="选择需要生成的规格"
+                  class="w-240px! mr-10px"
+                >
+                  <ElOption
+                    v-for="(item, ind) in selectRuleList"
+                    :key="ind"
+                    :label="item.rule_name"
+                    :value="ind"
+                  />
+                </ElSelect>
+                <BaseButton @click="selectAttrs(selectRuleIndex)">确定</BaseButton>
+                <BaseButton>添加新规格</BaseButton>
+              </div>
 
-          <!-- <ElFormItem label="规格类型" prop="spec_type">
-            <el-radio-group v-model="formData.spec_type">
-              <el-radio-button label="单规格" :value="0" />
-              <el-radio-button label="多规格" :value="1" />
-            </el-radio-group>
-          </ElFormItem> -->
-
-          <!-- <ElFormItem v-if="formData.spec_type == 1" label="" prop="spec_type">
-            <BaseButton>添加新规格</BaseButton>
-            <BaseButton>立即生成</BaseButton>
-          </ElFormItem> -->
+              <template v-if="actionAttrs.length > 0">
+                <div class="mt-10px w-full" v-for="(item, index) in actionAttrs" :key="index">
+                  <div class="w-100 mb-10px!">
+                    <ElTag
+                      type="info"
+                      effect="plain"
+                      class="mr-10px!"
+                      closable
+                      @close="delAttrlist(index)"
+                    >
+                      {{ item.value }}
+                    </ElTag>
+                  </div>
+                  <ElTag
+                    class="mr-10px! mb-10px!"
+                    v-for="(name, ind) in item.detail"
+                    :key="ind"
+                    closable
+                    @close="delAttrlistItem(name, item)"
+                  >
+                    {{ name }}
+                  </ElTag>
+                  <ElInput
+                    v-model="item.detailValue"
+                    class="w-20! mr-10px! mb-10px!"
+                    size="small"
+                    @keyup.enter="addAttrItem(item)"
+                  />
+                  <BaseButton
+                    type="primary"
+                    class="mb-10px!"
+                    size="small"
+                    @click="addAttrItem(item)"
+                  >
+                    添加
+                  </BaseButton>
+                </div>
+                <BaseButton type="primary" class="mb-10px!" @click="attrsConfirm">
+                  立即生成
+                </BaseButton>
+                <BaseButton type="primary" class="mb-10px!" @click="addAttrShow = true">
+                  添加新规格
+                </BaseButton>
+              </template>
+            </ElFormItem>
+            <template v-if="addAttrShow">
+              <ElFormItem label="规格">
+                <ElInput v-model="attrItem.name" placeholder="请输入规格名称" />
+              </ElFormItem>
+              <ElFormItem label="规格值">
+                <ElInput v-model="attrItem.value" placeholder="请输入规格参数" />
+              </ElFormItem>
+              <div class="w-full flex flex-justify-center">
+                <BaseButton type="primary" class="mb-10px!" @click="addAttr"> 确定 </BaseButton>
+                <BaseButton class="mb-10px!" @click="addAttrShow = false"> 取消 </BaseButton>
+              </div>
+            </template>
+            <template v-if="productAttrList.header.length > 0">
+              <ElFormItem label="批量设置">
+                <ElTable
+                  header-cell-class-name="bg-gray-100!"
+                  :data="attrBase"
+                  class="w-100%"
+                  :border="true"
+                  stripe
+                >
+                  <ElTableColumn prop="pic" label="图片" width="100px">
+                    <template #header> <span>图片</span><span class="text-red">*</span> </template>
+                    <template #default="{ row, $index }">
+                      <TableImage
+                        :isShowImageViewer="false"
+                        :src="row.pic[0]"
+                        @click="clickUpImage($index, 'attrBase')"
+                      />
+                    </template>
+                  </ElTableColumn>
+                  <ElTableColumn prop="price" label="售价">
+                    <template #header> <span>售价</span><span class="text-red">*</span> </template>
+                    <template #default="{ row }">
+                      <ElInput type="number" v-model="row.price" />
+                    </template>
+                  </ElTableColumn>
+                  <!-- <ElTableColumn prop="integral" label="积分">
+                  <template #default="{ row }">
+                    <ElInput type="number" v-model="row.integral" />
+                  </template>
+                </ElTableColumn> -->
+                  <ElTableColumn prop="cost" label="成本价">
+                    <template #default="{ row }">
+                      <ElInput type="number" v-model="row.cost" />
+                    </template>
+                  </ElTableColumn>
+                  <ElTableColumn prop="ot_price" label="原价">
+                    <template #header> <span>原价</span><span class="text-red">*</span> </template>
+                    <template #default="{ row }">
+                      <ElInput type="number" v-model="row.ot_price" />
+                    </template>
+                  </ElTableColumn>
+                  <ElTableColumn prop="stock" label="库存">
+                    <template #header> <span>库存</span><span class="text-red">*</span> </template>
+                    <template #default="{ row }">
+                      <ElInput type="number" v-model="row.stock" />
+                    </template>
+                  </ElTableColumn>
+                  <ElTableColumn prop="bar_code" label="产品编号">
+                    <template #default="{ row }">
+                      <ElInput v-model="row.bar_code" />
+                    </template>
+                  </ElTableColumn>
+                  <ElTableColumn prop="weight" label="重量">
+                    <template #default="{ row }">
+                      <ElInput v-model="row.weight" />
+                    </template>
+                  </ElTableColumn>
+                  <ElTableColumn prop="volume" label="体积">
+                    <template #default="{ row }">
+                      <ElInput v-model="row.volume" />
+                    </template>
+                  </ElTableColumn>
+                  <ElTableColumn fixed="right" label="操作" width="140px">
+                    <BaseButton link type="primary" @click="batchSet"> 批量设置 </BaseButton>
+                    <ElDivider direction="vertical" />
+                    <BaseButton @click="initAttrBase" link type="primary"> 清空 </BaseButton>
+                  </ElTableColumn>
+                </ElTable>
+              </ElFormItem>
+              <ElFormItem label="商品属性">
+                <ElTable
+                  header-cell-class-name="bg-gray-100!"
+                  :data="productAttrList.value"
+                  class="w-100%"
+                  :border="true"
+                  stripe
+                >
+                  <template v-for="(item, index) in productAttrList.header" :key="index">
+                    <ElTableColumn
+                      v-if="item.slot !== 'action'"
+                      :label="item.title"
+                      :minWidth="item.minWidth"
+                    >
+                      <template #default="{ row, $index }">
+                        <span v-if="item.key">
+                          {{ row[item.key] }}
+                        </span>
+                        <template v-else-if="item.slot">
+                          <TableImage
+                            :isShowImageViewer="false"
+                            :src="row.pic[0]"
+                            v-if="item.slot == 'pic'"
+                            @click="clickUpImage($index, 'header')"
+                          />
+                          <template v-else-if="item.slot == 'action'">
+                            <BaseButton
+                              @click="delAttrItem($index, productAttrList.value)"
+                              link
+                              type="primary"
+                            >
+                              删除
+                            </BaseButton>
+                          </template>
+                          <ElInput v-else v-model="row[item.slot]" />
+                        </template>
+                      </template>
+                    </ElTableColumn>
+                    <ElTableColumn
+                      fixed="right"
+                      v-else
+                      :label="item.title"
+                      :minWidth="item.minWidth"
+                    >
+                      <template #default="{ $index }">
+                        <BaseButton
+                          @click="delAttrItem($index, productAttrList.value)"
+                          link
+                          type="primary"
+                        >
+                          删除
+                        </BaseButton>
+                      </template>
+                    </ElTableColumn>
+                  </template>
+                </ElTable>
+              </ElFormItem>
+            </template>
+          </template>
         </ElTabPane>
         <ElTabPane label="商品详情" :name="3">
           <Editor v-model="formData.description" ref="editorRef" />
@@ -462,6 +970,12 @@ const getStore = async (query = '', id = '') => {
         </ElTabPane>
       </ElTabs>
     </ElForm>
+    <UpImgButtom
+      class="pos-absolute left-[-200px]"
+      ref="upImageButtomRef"
+      v-model="selectPic"
+      @change="changePic"
+    />
   </ContentWrap>
   <div
     class="text-center border-t-1px border-solid border-gray-200 py-10px mt-10px position-sticky bottom-0 left-0 right-0 bg-white"

+ 208 - 0
src/views/Goods/rule/components/Write.vue

@@ -0,0 +1,208 @@
+<script setup lang="tsx">
+import { watch, ref, reactive } from 'vue'
+import { productRule, productRuleValue } from '@/api/goods/types'
+import { getProductRuleDetail, putProductRule, addProductRule } from '@/api/goods'
+import { ElForm, ElFormItem, ElInput, ElDivider, ElTag, ElMessage } from 'element-plus'
+import type { FormRules, FormInstance } from 'element-plus'
+import { Dialog } from '@/components/Dialog'
+import { useI18n } from '@/hooks/web/useI18n'
+const { t } = useI18n()
+
+const props = defineProps({
+  id: {
+    type: Number,
+    default: 0
+  },
+  title: {
+    type: String,
+    default: ''
+  }
+})
+const modelValue = defineModel<boolean>()
+const ruleFormRef = ref<FormInstance>()
+const submit = async () => {
+  if (ruleFormRef.value) {
+    const valid = await ruleFormRef.value.validate((valid, fields) => {
+      // console.log(fields, 'fields')
+      if (!valid) {
+        console.log('error submit!', fields)
+      }
+    })
+    if (valid) {
+      loadingData.value = true
+      try {
+        let re: any = {}
+        if (form.value.id > 0) {
+          re = await putProductRule(form.value)
+        } else if (form.value.id == 0) {
+          re = await addProductRule(form.value)
+        }
+        if (re.status == 200) {
+          ElMessage({
+            showClose: true,
+            message: '添加成功',
+            type: 'success'
+          })
+        }
+        emit('submit')
+      } catch (error) {
+        console.log(error)
+      } finally {
+        loadingData.value = false
+      }
+    }
+  }
+}
+const loadingData = ref(false)
+watch(
+  () => props.id,
+  async (value) => {
+    if (value * 1 === 0) return
+    loadingData.value = true
+    const res = await getProductRuleDetail(value)
+    form.value = res.data
+    loadingData.value = false
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+const form = ref<productRule>({
+  id: 0,
+  rule_name: '',
+  rule_value: []
+})
+const rule_type = ref({
+  name: '',
+  value: ''
+})
+const addShow = ref(false)
+const emit = defineEmits(['submit'])
+const rules = reactive<FormRules<productRule>>({
+  rule_name: [{ required: true, trigger: 'blur' }],
+  rule_value: [
+    {
+      required: true,
+      validator: (_, __, callback: any) => {
+        // console.log(2333)
+        if (form.value.rule_value.length < 1) {
+          ElMessage.error('请添加规格参数')
+          return callback(new Error('请添加规格参数'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ]
+})
+const addType = () => {
+  form.value.rule_value.push({
+    attrHidden: '',
+    detail: [rule_type.value.value],
+    detailValue: '',
+    value: rule_type.value.name
+  })
+  rule_type.value.name = ''
+  rule_type.value.value = ''
+  addShow.value = false
+}
+const handleClose = (name: string, tag: productRuleValue) => {
+  tag.detail.splice(tag.detail.indexOf(name), 1)
+}
+const handleCloseTag = (tag: productRuleValue) => {
+  form.value.rule_value.splice(form.value.rule_value.indexOf(tag), 1)
+}
+
+const handleInputConfirm = (tag: productRuleValue) => {
+  if (tag.detail.indexOf(tag.detailValue) > -1) {
+    ElMessage.error('规格参数已存在')
+    return
+  }
+  if (tag.detailValue) {
+    tag.detail.push(tag.detailValue)
+  }
+  tag.attrHidden = ''
+  tag.detailValue = ''
+}
+</script>
+
+<template>
+  <Dialog v-model="modelValue" :title="title" width="600px">
+    <el-form
+      v-loading="loadingData"
+      ref="ruleFormRef"
+      :model="form"
+      label-width="auto"
+      :rules="rules"
+    >
+      <el-form-item
+        prop="rule_name"
+        label="规格模版名称"
+        :rules="[{ required: true, message: '请输入规格名', trigger: 'blur' }]"
+      >
+        <el-input v-model="form.rule_name" />
+      </el-form-item>
+      <el-form-item v-show="form.rule_value.length > 0" label="规格参数" prop="rule_value">
+        <template v-for="(tag, index) in form.rule_value" :key="index">
+          <div class="w-100 mb-10px!">
+            <ElTag
+              type="info"
+              effect="plain"
+              class="mr-10px!"
+              closable
+              @close="handleCloseTag(tag)"
+            >
+              {{ tag.value }}
+            </ElTag>
+          </div>
+          <ElTag
+            class="mr-10px! mb-10px!"
+            v-for="(name, ind) in tag.detail"
+            :key="ind"
+            closable
+            @close="handleClose(name, tag)"
+          >
+            {{ name }}
+          </ElTag>
+          <el-input
+            v-model="tag.detailValue"
+            class="w-20! mr-10px! mb-10px!"
+            size="small"
+            @keyup.enter="handleInputConfirm(tag)"
+          />
+          <BaseButton type="primary" class="mb-10px!" size="small" @click="handleInputConfirm(tag)">
+            添加
+          </BaseButton>
+        </template>
+      </el-form-item>
+      <template v-if="addShow">
+        <el-form-item label="规格名称">
+          <el-input v-model="rule_type.name" />
+        </el-form-item>
+        <el-form-item label="规格参数">
+          <el-input v-model="rule_type.value" />
+        </el-form-item>
+        <el-form-item>
+          <div class="flex flex-justify-end w-100%">
+            <BaseButton link type="primary" @click="addType">添加</BaseButton>
+            <ElDivider direction="vertical" />
+            <BaseButton link type="primary" @click="addShow = false">取消</BaseButton>
+          </div>
+        </el-form-item>
+      </template>
+      <template v-else>
+        <el-form-item>
+          <BaseButton type="success" @click="addShow = true">添加新规格</BaseButton>
+        </el-form-item>
+      </template>
+    </el-form>
+    <template #footer>
+      <BaseButton type="primary" :loading="loadingData" @click="submit">
+        {{ t('exampleDemo.save') }}
+      </BaseButton>
+      <BaseButton @click="modelValue = false">{{ t('dialogDemo.close') }}</BaseButton>
+    </template>
+  </Dialog>
+</template>

+ 191 - 0
src/views/Goods/rule/index.vue

@@ -0,0 +1,191 @@
+<script setup lang="tsx">
+import { reactive, ref, unref, useTemplateRef } from 'vue'
+import { useTable } from '@/hooks/web/useTable'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Table, TableColumn } from '@/components/Table'
+import { ContentWrap } from '@/components/ContentWrap'
+import { BaseButton } from '@/components/Button'
+import { ElDivider, ElMessage, ElMessageBox } from 'element-plus'
+import Write from './components/Write.vue'
+import { delProductRule, getProductRule } from '@/api/goods'
+import { FormSchema } from '@/components/Form'
+import { Search } from '@/components/Search'
+
+const { t } = useI18n()
+
+const { tableRegister, tableState, tableMethods } = useTable({
+  fetchDataApi: async () => {
+    const res = await getProductRule({
+      ...searchParams.value,
+      page: unref(currentPage) || 1,
+      limit: unref(pageSize) || 10
+    })
+    return {
+      list: res.data,
+      total: res.data.length || 0
+    }
+  }
+})
+
+const { dataList, loading, total, currentPage, pageSize } = tableState
+const { getList } = tableMethods
+
+const tableColumns = reactive<TableColumn[]>([
+  {
+    field: 'id',
+    label: 'ID',
+    width: 100
+  },
+  {
+    field: 'rule_name',
+    label: '规格模版名称',
+    minWidth: 100
+  },
+  {
+    field: 'rule_value',
+    label: '规格参数',
+    minWidth: 140,
+    slots: {
+      default: (data: any) => {
+        const row = data.row
+        return (
+          <>
+            <div>
+              {row.rule_value.map((item: any) => {
+                return (
+                  <>
+                    <div>
+                      {item.value}:
+                      {item.detail.map((its: any, ind: number) => {
+                        return (
+                          <>
+                            <span>{its}</span>
+                            {ind != item.detail.length - 1 ? '、' : ''}
+                          </>
+                        )
+                      })}
+                    </div>
+                  </>
+                )
+              })}
+            </div>
+          </>
+        )
+      }
+    }
+  },
+  {
+    field: 'action',
+    label: t('userDemo.action'),
+    width: 200,
+    fixed: 'right',
+    align: 'center',
+    headerAlign: 'center',
+    slots: {
+      default: (data: any) => {
+        const row = data.row
+        return (
+          <>
+            <BaseButton link size="small" type="primary" onClick={() => action('edit', row)}>
+              编辑
+            </BaseButton>
+            <ElDivider direction="vertical" />
+            <BaseButton link size="small" type="danger" onClick={() => delAction(row)}>
+              删除
+            </BaseButton>
+          </>
+        )
+      }
+    }
+  }
+])
+
+const dialogVisible = ref(false)
+const currentRow = ref(0)
+const dialogTitle = ref('')
+const actionType = ref('')
+const writeRef = useTemplateRef('writeRef')
+
+const action = async (type: string, row?: any) => {
+  actionType.value = type
+  if (type == 'add') {
+    dialogTitle.value = t('exampleDemo.add')
+    currentRow.value = 0
+  }
+  if (type == 'edit') {
+    dialogTitle.value = t('exampleDemo.edit')
+    currentRow.value = row.id
+  }
+  dialogVisible.value = true
+}
+const delAction = (row: any) => {
+  ElMessageBox.confirm('删除后无法恢复,是否删除?', {
+    confirmButtonText: '删除',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      const re = await delProductRule(row.id)
+      if (re) {
+        ElMessage({
+          showClose: true,
+          message: '删除成功',
+          type: 'success'
+        })
+      }
+      await getList()
+    })
+    .catch(() => {})
+}
+const save = async () => {
+  getList()
+  dialogVisible.value = false
+}
+const searchSchema = reactive<FormSchema[]>([
+  {
+    field: 'rule_name',
+    label: '规格名称',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入规格名称'
+    }
+  }
+])
+const searchParams = ref<{ rule_name: string }>({ rule_name: '' })
+
+const setSearchParams = (data: any) => {
+  searchParams.value = data
+  getList()
+}
+</script>
+
+<template>
+  <ContentWrap>
+    <Search :schema="searchSchema" @reset="setSearchParams" @search="setSearchParams" />
+
+    <div class="mb-10px">
+      <BaseButton type="primary" @click="action('add')">{{ t('exampleDemo.add') }}</BaseButton>
+    </div>
+    <Table
+      v-model:current-page="currentPage"
+      v-model:page-size="pageSize"
+      :columns="tableColumns"
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      stripe
+      :data="dataList"
+      :loading="loading"
+      @register="tableRegister"
+      :pagination="{
+        total
+      }"
+    />
+  </ContentWrap>
+
+  <Write
+    v-model="dialogVisible"
+    :title="dialogTitle"
+    ref="writeRef"
+    :id="currentRow"
+    @submit="save"
+  />
+</template>

+ 0 - 1
src/views/Plan/list/index.vue

@@ -289,7 +289,6 @@ const changeStatus = async (is_recommend, row) => {
     })
   } catch (error) {
     console.log(error)
-  } finally {
   }
 }
 const save = async () => {

+ 6 - 1
src/views/User/list/index.vue

@@ -40,7 +40,7 @@ import BatchSet from './components/BatchSet.vue'
 import { useSearch } from '@/hooks/web/useSearch'
 import Icon from '@/components/Icon/src/Icon.vue'
 const { searchRegister, searchMethods } = useSearch()
-const { setSchema } = searchMethods
+const { setSchema, setValues } = searchMethods
 searchTime.field = 'user_time'
 const { t } = useI18n()
 const groupList = ref<any[]>([])
@@ -121,6 +121,11 @@ const searchSchema = reactive<FormSchema[]>([
                 v-model={searchParams.value.field_key}
                 placeholder="全部"
                 style={'width: 80px'}
+                onChange={(value) =>
+                  setValues({
+                    field_key: value
+                  })
+                }
               >
                 <ElOption label="全部" value="all" />
                 <ElOption label="昵称" value="nickname" />