浏览代码

!366 fix: 修复 CRM 的一些 bug
Merge pull request !366 from puhui999/dev-crm

芋道源码 1 年之前
父节点
当前提交
213af427a2

+ 2 - 0
src/api/crm/followup/index.ts

@@ -7,6 +7,8 @@ export interface FollowUpRecordVO {
   bizId: number // 数据编号
   type: number // 跟进类型
   content: string // 跟进内容
+  picUrls: string[]
+  fileUrls: string[]
   nextTime: Date // 下次联系时间
   businessIds: number[] // 关联的商机编号数组
   contactIds: number[] // 关联的联系人编号数组

+ 9 - 8
src/api/crm/permission/index.ts

@@ -2,14 +2,15 @@ import request from '@/config/axios'
 
 export interface PermissionVO {
   id?: number // 数据权限编号
-  userId: number | undefined // 用户编号
-  bizType: number | undefined // Crm 类型
-  bizId: number | undefined // Crm 类型数据编号
-  level: number | undefined // 权限级别
+  userId: number // 用户编号
+  bizType: number // Crm 类型
+  bizId: number // Crm 类型数据编号
+  level: number // 权限级别
   deptName?: string // 部门名称
   nickname?: string // 用户昵称
   postNames?: string[] // 岗位名称数组
   createTime?: Date
+  ids?: number[]
 }
 
 /**
@@ -50,11 +51,11 @@ export const updatePermission = async (data) => {
 }
 
 // 删除数据权限(删除团队成员)
-export const deletePermissionBatch = async (params) => {
-  return await request.delete({ url: '/crm/permission/delete', params })
+export const deletePermissionBatch = async (val: number[]) => {
+  return await request.delete({ url: '/crm/permission/delete?ids=' + val.join(',') })
 }
 
 // 删除自己的数据权限(退出团队)
-export const deleteSelfPermission = async (id) => {
-  return await request.delete({ url: '/crm/permission/quit-team?id=' + id })
+export const deleteSelfPermission = async (id: number) => {
+  return await request.delete({ url: '/crm/permission/delete-self?id=' + id })
 }

+ 43 - 25
src/components/UploadFile/src/UploadImgs.vue

@@ -28,7 +28,7 @@
             <Icon icon="ep:zoom-in" />
             <span>查看</span>
           </div>
-          <div class="handle-icon" @click="handleRemove(file)" v-if="!disabled">
+          <div v-if="!disabled" class="handle-icon" @click="handleRemove(file)">
             <Icon icon="ep:delete" />
             <span>删除</span>
           </div>
@@ -46,7 +46,6 @@
   </div>
 </template>
 <script lang="ts" setup>
-import { PropType } from 'vue'
 import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
 import { ElNotification } from 'element-plus'
 
@@ -70,10 +69,7 @@ type FileTypes =
   | 'image/x-icon'
 
 const props = defineProps({
-  modelValue: {
-    type: Array as PropType<UploadUserFile[]>,
-    required: true
-  },
+  modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
   updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
   drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true)
   disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
@@ -91,18 +87,8 @@ const uploadHeaders = ref({
 })
 
 const fileList = ref<UploadUserFile[]>([])
-// fix: 改为动态监听赋值解决图片回显问题
-watch(
-  () => props.modelValue,
-  (data) => {
-    if (!data) return
-    fileList.value = data
-  },
-  {
-    deep: true,
-    immediate: true
-  }
-)
+const uploadNumber = ref<number>(0)
+const uploadList = ref<UploadUserFile[]>([])
 /**
  * @description 文件上传之前判断
  * @param rawFile 上传的文件
@@ -122,29 +108,61 @@ const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
       message: `上传图片大小不能超过 ${props.fileSize}M!`,
       type: 'warning'
     })
+  uploadNumber.value++
   return imgType.includes(rawFile.type as FileTypes) && imgSize
 }
 
 // 图片上传成功
 interface UploadEmits {
-  (e: 'update:modelValue', value: UploadUserFile[]): void
+  (e: 'update:modelValue', value: string[]): void
 }
 
 const emit = defineEmits<UploadEmits>()
-const uploadSuccess = (response, uploadFile: UploadFile) => {
-  if (!response) return
-  // TODO 多图上传组件成功后只是把保存成功后的url替换掉组件选图时的文件路径,所以返回的fileList包含的是一个包含文件信息的对象列表
-  uploadFile.url = response.data
-  emit('update:modelValue', fileList.value)
+const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
   message.success('上传成功')
+  // 删除自身
+  debugger
+  const index = fileList.value.findIndex((item) => item.response?.data === res.data)
+  fileList.value.splice(index, 1)
+  uploadList.value.push({ name: res.data, url: res.data })
+  if (uploadList.value.length == uploadNumber.value) {
+    fileList.value.push(...uploadList.value)
+    uploadList.value = []
+    uploadNumber.value = 0
+    emitUpdateModelValue()
+  }
 }
 
+// 监听模型绑定值变动
+watch(
+  () => props.modelValue,
+  (val: string | string[]) => {
+    if (!val) {
+      fileList.value = [] // fix:处理掉缓存,表单重置后上传组件的内容并没有重置
+      return
+    }
+
+    fileList.value = [] // 保障数据为空
+    fileList.value.push(
+      ...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
+    )
+  },
+  { immediate: true, deep: true }
+)
+// 发送图片链接列表更新
+const emitUpdateModelValue = () => {
+  let result: string[] = fileList.value.map((file) => file.url!)
+  emit('update:modelValue', result)
+}
 // 删除图片
 const handleRemove = (uploadFile: UploadFile) => {
   fileList.value = fileList.value.filter(
     (item) => item.url !== uploadFile.url || item.name !== uploadFile.name
   )
-  emit('update:modelValue', fileList.value)
+  emit(
+    'update:modelValue',
+    fileList.value.map((file) => file.url!)
+  )
 }
 
 // 图片上传错误提示

+ 1 - 0
src/views/crm/customer/detail/index.vue

@@ -51,6 +51,7 @@
           :biz-id="customer.id!"
           :biz-type="BizTypeEnum.CRM_CUSTOMER"
           :show-action="!permissionListRef?.isPool || false"
+          @quit-team="close"
         />
       </el-tab-pane>
       <el-tab-pane label="商机" lazy>

+ 30 - 40
src/views/crm/followup/FollowUpRecordForm.vue

@@ -31,52 +31,38 @@
             />
           </el-form-item>
         </el-col>
-        <!-- TODO @puhui999:不搞富文本哈;然后加个附件、图片两个 form-item 哈 -->
         <el-col :span="24">
           <el-form-item label="跟进内容" prop="content">
-            <Editor v-model="formData.content" height="300px" />
+            <el-input v-model="formData.content" :rows="3" type="textarea" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="图片" prop="content">
+            <UploadImgs v-model="formData.picUrls" class="min-w-80px" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="附件" prop="content">
+            <UploadFile v-model="formData.fileUrls" class="min-w-80px" />
           </el-form-item>
         </el-col>
-        <!-- TODO @puhui999:因为不考虑编辑的情况,是不是关联要是个弹窗选择哈? -->
         <el-col :span="24">
           <el-form-item label="关联联系人" prop="contactIds">
-            <el-select v-model="formData.contactIds" multiple placeholder="请选择">
-              <el-option
-                v-for="item in allContactList"
-                :key="item.id"
-                :label="item.name"
-                :value="item.id"
-              />
-            </el-select>
+            <el-button @click="handleAddContact">
+              <Icon class="mr-5px" icon="ep:plus" />
+              添加联系人
+            </el-button>
             <contact-list v-model:contactIds="formData.contactIds" />
           </el-form-item>
-          <!--          <el-form-item label="关联联系人" prop="contactIds">-->
-          <!--            <el-button @click="handleAddContact">-->
-          <!--              <Icon class="mr-5px" icon="ep:plus" />-->
-          <!--              选择添加联系人-->
-          <!--            </el-button>-->
-          <!--            <contact-list v-model:contactIds="formData.contactIds" />-->
-          <!--          </el-form-item>-->
         </el-col>
         <el-col :span="24">
           <el-form-item label="关联商机" prop="businessIds">
-            <el-select v-model="formData.businessIds" multiple placeholder="请选择">
-              <el-option
-                v-for="item in allBusinessList"
-                :key="item.id"
-                :label="item.name"
-                :value="item.id"
-              />
-            </el-select>
+            <el-button @click="handleAddBusiness">
+              <Icon class="mr-5px" icon="ep:plus" />
+              添加商机
+            </el-button>
             <business-list v-model:businessIds="formData.businessIds" />
           </el-form-item>
-          <!--          <el-form-item label="关联商机" prop="businessIds">-->
-          <!--            <el-button @click="handleAddBusiness">-->
-          <!--              <Icon class="mr-5px" icon="ep:plus" />-->
-          <!--              选择添加商机-->
-          <!--            </el-button>-->
-          <!--            <business-list v-model:businessIds="formData.businessIds" />-->
-          <!--          </el-form-item>-->
         </el-col>
       </el-row>
     </el-form>
@@ -85,13 +71,13 @@
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
+  <ContactTableSelect ref="contactTableSelectRef" v-model="formData.contactIds" />
+  <BusinessTableSelect ref="businessTableSelectRef" v-model="formData.businessIds" />
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { FollowUpRecordApi, FollowUpRecordVO } from '@/api/crm/followup'
-import { BusinessList, ContactList } from './components'
-import * as ContactApi from '@/api/crm/contact'
-import * as BusinessApi from '@/api/crm/business'
+import { BusinessList, BusinessTableSelect, ContactList, ContactTableSelect } from './components'
 
 defineOptions({ name: 'FollowUpRecordForm' })
 
@@ -110,8 +96,6 @@ const formRules = reactive({
 })
 
 const formRef = ref() // 表单 Ref
-const allContactList = ref<ContactApi.ContactVO[]>([]) // 所有联系人列表
-const allBusinessList = ref<BusinessApi.BusinessVO[]>([]) // 所有商家列表
 
 /** 打开弹窗 */
 const open = async (bizType: number, bizId: number, type: string, id?: number) => {
@@ -121,8 +105,6 @@ const open = async (bizType: number, bizId: number, type: string, id?: number) =
   resetForm()
   formData.value.bizType = bizType
   formData.value.bizId = bizId
-  allContactList.value = await ContactApi.getSimpleContactList()
-  allBusinessList.value = await BusinessApi.getSimpleBusinessList()
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
@@ -159,6 +141,14 @@ const submitForm = async () => {
   }
 }
 
+const contactTableSelectRef = ref<InstanceType<typeof ContactTableSelect>>()
+const handleAddContact = () => {
+  contactTableSelectRef.value?.open()
+}
+const businessTableSelectRef = ref<InstanceType<typeof BusinessTableSelect>>()
+const handleAddBusiness = () => {
+  businessTableSelectRef.value?.open()
+}
 /** 重置表单 */
 const resetForm = () => {
   formRef.value?.resetFields()

+ 1 - 1
src/views/crm/followup/components/BusinessList.vue

@@ -52,7 +52,7 @@ watch(
     if (!val || val.length === 0) {
       return
     }
-    list.value = BusinessApi.getBusinessListByIds(val) as unknown as BusinessApi.BusinessVO[]
+    list.value = BusinessApi.getBusinessListByIds(unref(val)) as unknown as BusinessApi.BusinessVO[]
   }
 )
 const emits = defineEmits<{

+ 0 - 79
src/views/crm/followup/components/BusinessListSelectForm.vue

@@ -1,79 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
-    <el-row>
-      <el-col :span="12">
-        <el-form-item label="跟进类型" prop="type">
-          <el-select v-model="formData.type" placeholder="请选择跟进类型">
-            <el-option
-              v-for="dict in getIntDictOptions(DICT_TYPE.CRM_FOLLOW_UP_TYPE)"
-              :key="dict.value"
-              :label="dict.label"
-              :value="dict.value"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="下次联系时间" prop="nextTime">
-          <el-date-picker
-            v-model="formData.nextTime"
-            placeholder="选择下次联系时间"
-            type="date"
-            value-format="x"
-          />
-        </el-form-item>
-      </el-col>
-    </el-row>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-/** 跟进记录 表单 */
-defineOptions({ name: 'BusinessListSelectForm' })
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formData = ref([])
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await FollowUpRecordApi.getFollowUpRecord(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
-  // 校验表单
-  await formRef.value.validate()
-  // 提交请求
-  formLoading.value = true
-  try {
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formRef.value?.resetFields()
-}
-</script>

+ 84 - 0
src/views/crm/followup/components/BusinessTableSelect.vue

@@ -0,0 +1,84 @@
+<template>
+  <Dialog v-model="dialogVisible" :appendToBody="true" title="选择商机" width="700">
+    <el-table
+      ref="multipleTableRef"
+      v-loading="loading"
+      :data="list"
+      :show-overflow-tooltip="true"
+      :stripe="true"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" />
+      <el-table-column align="center" label="商机名称" prop="name" />
+      <el-table-column align="center" label="客户名称" prop="customerName" />
+      <el-table-column align="center" label="商机金额" prop="price" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="预计成交日期"
+        prop="dealTime"
+        width="180px"
+      />
+      <el-table-column align="center" label="备注" prop="remark" />
+    </el-table>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import * as BusinessApi from '@/api/crm/business'
+import { dateFormatter } from '@/utils/formatTime'
+import { ElTable } from 'element-plus'
+
+defineOptions({ name: 'BusinessTableSelect' })
+withDefaults(defineProps<{ modelValue: number[] }>(), { modelValue: () => [] })
+const list = ref<BusinessApi.BusinessVO[]>([]) // 列表的数据
+const loading = ref(false) // 列表的加载中
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false)
+// 确认选择时的触发事件
+const emits = defineEmits<{
+  (e: 'update:modelValue', v: number[]): void
+}>()
+const multipleTableRef = ref<InstanceType<typeof ElTable>>()
+const multipleSelection = ref<BusinessApi.BusinessVO[]>([])
+const handleSelectionChange = (val: BusinessApi.BusinessVO[]) => {
+  multipleSelection.value = val
+}
+/** 触发 */
+const submitForm = () => {
+  formLoading.value = true
+  try {
+    emits(
+      'update:modelValue',
+      multipleSelection.value.map((item) => item.id)
+    )
+  } finally {
+    formLoading.value = false
+    // 关闭弹窗
+    dialogVisible.value = false
+  }
+}
+
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await BusinessApi.getSimpleBusinessList()
+  } finally {
+    loading.value = false
+  }
+}
+/** 打开弹窗 */
+const open = async () => {
+  dialogVisible.value = true
+  await nextTick()
+  if (multipleSelection.value.length > 0) {
+    multipleTableRef.value!.clearSelection()
+  }
+  await getList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

+ 1 - 1
src/views/crm/followup/components/ContactList.vue

@@ -69,7 +69,7 @@ const props = withDefaults(defineProps<{ contactIds: number[] }>(), {
 const list = ref<ContactApi.ContactVO[]>([] as ContactApi.ContactVO[])
 const getContactList = async () => {
   list.value = (await ContactApi.getContactListByIds(
-    props.contactIds
+    unref(props.contactIds)
   )) as unknown as ContactApi.ContactVO[]
 }
 watch(

+ 83 - 0
src/views/crm/followup/components/ContactTableSelect.vue

@@ -0,0 +1,83 @@
+<template>
+  <Dialog v-model="dialogVisible" :appendToBody="true" title="选择联系人" width="700">
+    <el-table
+      ref="multipleTableRef"
+      v-loading="loading"
+      :data="list"
+      :show-overflow-tooltip="true"
+      :stripe="true"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" />
+      <el-table-column align="center" fixed="left" label="姓名" prop="name" width="140" />
+      <el-table-column
+        align="center"
+        fixed="left"
+        label="客户名称"
+        prop="customerName"
+        width="120"
+      />
+      <el-table-column align="center" label="手机" prop="mobile" width="120" />
+      <el-table-column align="center" label="电话" prop="telephone" width="120" />
+      <el-table-column align="center" label="邮箱" prop="email" width="120" />
+      <el-table-column align="center" label="职位" prop="post" width="120" />
+    </el-table>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import * as ContactApi from '@/api/crm/contact'
+import { ElTable } from 'element-plus'
+
+defineOptions({ name: 'ContactTableSelect' })
+withDefaults(defineProps<{ modelValue: number[] }>(), { modelValue: () => [] })
+const list = ref<ContactApi.ContactVO[]>([]) // 列表的数据
+const loading = ref(false) // 列表的加载中
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false)
+// 确认选择时的触发事件
+const emits = defineEmits<{
+  (e: 'update:modelValue', v: number[]): void
+}>()
+const multipleTableRef = ref<InstanceType<typeof ElTable>>()
+const multipleSelection = ref<ContactApi.ContactVO[]>([])
+const handleSelectionChange = (val: ContactApi.ContactVO[]) => {
+  multipleSelection.value = val
+}
+/** 触发 */
+const submitForm = () => {
+  formLoading.value = true
+  try {
+    emits(
+      'update:modelValue',
+      multipleSelection.value.map((item) => item.id)
+    )
+  } finally {
+    formLoading.value = false
+    // 关闭弹窗
+    dialogVisible.value = false
+  }
+}
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await ContactApi.getSimpleContactList()
+  } finally {
+    loading.value = false
+  }
+}
+/** 打开弹窗 */
+const open = async () => {
+  dialogVisible.value = true
+  await nextTick()
+  if (multipleSelection.value.length > 0) {
+    multipleTableRef.value!.clearSelection()
+  }
+  await getList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

+ 3 - 1
src/views/crm/followup/components/index.ts

@@ -1,4 +1,6 @@
 import BusinessList from './BusinessList.vue'
+import BusinessTableSelect from './BusinessTableSelect.vue'
 import ContactList from './ContactList.vue'
+import ContactTableSelect from './ContactTableSelect.vue'
 
-export { BusinessList, ContactList }
+export { BusinessList, BusinessTableSelect, ContactList, ContactTableSelect }

+ 4 - 4
src/views/crm/message/tables/TodayCustomer.vue

@@ -15,9 +15,9 @@
         <el-select v-model="queryParams.contactStatus" class="!w-240px" placeholder="状态">
           <el-option
             v-for="(option, index) in CONTACT_STATUS"
+            :key="index"
             :label="option.label"
             :value="option.value"
-            :key="index"
           />
         </el-select>
       </el-form-item>
@@ -25,9 +25,9 @@
         <el-select v-model="queryParams.sceneType" class="!w-240px" placeholder="归属">
           <el-option
             v-for="(option, index) in SCENE_TYPES"
+            :key="index"
             :label="option.label"
             :value="option.value"
-            :key="index"
           />
         </el-select>
       </el-form-item>
@@ -84,7 +84,7 @@
           <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
         </template>
       </el-table-column>
-      <!-- TODO @puhui999:距进入公海天数 -->
+      <el-table-column align="center" label="距进入公海天数" prop="poolDay" width="100px" />
       <el-table-column
         :formatter="dateFormatter"
         align="center"
@@ -120,7 +120,7 @@
   </ContentWrap>
 </template>
 
-<script lang="ts" setup name="TodayCustomer">
+<script lang="ts" name="TodayCustomer" setup>
 import { DICT_TYPE } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import * as MessageApi from '@/api/crm/message'

+ 27 - 15
src/views/crm/permission/components/PermissionForm.vue

@@ -17,7 +17,6 @@
           />
         </el-select>
       </el-form-item>
-      <!-- TODO @puhui999:编辑时,level 没带过来 -->
       <el-form-item label="权限级别" prop="level">
         <el-radio-group v-model="formData.level">
           <template
@@ -30,7 +29,13 @@
           </template>
         </el-radio-group>
       </el-form-item>
-      <!-- TODO @puhui999:同时添加至 -->
+      <!-- TODO @puhui999:同时添加至,还没想好下次搞 -->
+      <el-form-item v-if="formType === 'create'" label="同时添加至" prop="toBizType">
+        <el-select v-model="formData.userId">
+          <el-option :value="1" label="联系人" />
+          <el-option :value="1" label="商机" />
+        </el-select>
+      </el-form-item>
     </el-form>
     <template #footer>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -54,12 +59,7 @@ const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-const formData = ref<PermissionApi.PermissionVO & { ids?: number[] }>({
-  userId: undefined, // 用户编号
-  bizType: undefined, // CRM 类型
-  bizId: undefined, // CRM 类型数据编号
-  level: undefined // 权限级别
-})
+const formData = ref<PermissionApi.PermissionVO>({} as PermissionApi.PermissionVO)
 const formRules = reactive({
   userId: [{ required: true, message: '人员不能为空', trigger: 'blur' }],
   level: [{ required: true, message: '权限级别不能为空', trigger: 'blur' }]
@@ -77,7 +77,23 @@ const open = async (type: 'create' | 'update', bizType: number, bizId: number, i
     formData.value.ids = ids
   }
 }
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+/** 打开修改权限弹窗 */
+const open0 = async (
+  type: 'create' | 'update',
+  bizType: number,
+  bizId: number,
+  id: number,
+  level: number
+) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type) + '团队成员'
+  formType.value = type
+  resetForm(bizType, bizId)
+  // 修改时,设置数据
+  formData.value.level = level
+  formData.value.ids = [id]
+}
+defineExpose({ open, open0 }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
@@ -108,12 +124,8 @@ const submitForm = async () => {
 /** 重置表单 */
 const resetForm = (bizType: number, bizId: number) => {
   formRef.value?.resetFields()
-  formData.value = {
-    userId: undefined, // 用户编号
-    bizType, // Crm 类型
-    bizId, // Crm 类型数据编号
-    level: undefined // 权限级别
-  }
+  formData.value = {} as PermissionApi.PermissionVO
+  formData.value = { ...formData.value, bizType, bizId }
 }
 onMounted(async () => {
   // 获得用户列表

+ 23 - 10
src/views/crm/permission/components/PermissionList.vue

@@ -105,8 +105,17 @@ const handleUpdate = () => {
     message.warning('请先选择团队成员后操作!')
     return
   }
-  const ids = multipleSelection.value?.map((item) => item.id) as unknown as number[]
-  formRef.value?.open('update', props.bizType, props.bizId!, ids)
+  if (multipleSelection.value?.length > 1) {
+    message.warning('编辑团队成员时只能选择一个!')
+    return
+  }
+  formRef.value?.open0(
+    'update',
+    props.bizType,
+    props.bizId!,
+    multipleSelection.value[0].id!,
+    multipleSelection.value[0].level
+  )
 }
 
 /** 移除团队成员 */
@@ -116,12 +125,10 @@ const handleDelete = async () => {
     return
   }
   await message.delConfirm()
-  const ids = multipleSelection.value?.map((item) => item.id)
-  await PermissionApi.deletePermissionBatch({
-    bizType: props.bizType,
-    bizId: props.bizId,
-    ids
-  })
+  const ids = multipleSelection.value?.map((item) => item.id) as unknown as number[]
+  await PermissionApi.deletePermissionBatch(ids)
+  message.success('移除团队成员成功!')
+  await getList()
 }
 
 /** 添加团队成员 */
@@ -164,6 +171,9 @@ watch(
 )
 
 defineExpose({ openForm, validateOwnerUser, validateWrite, isPool })
+const emits = defineEmits<{
+  (e: 'quitTeam'): void
+}>()
 /** 退出团队 */
 const handleQuit = async () => {
   const permission = list.value.find(
@@ -175,9 +185,12 @@ const handleQuit = async () => {
     return
   }
   const userPermission = list.value.find((item) => item.userId === userStore.getUser.id)
-  if (userPermission) {
-    await PermissionApi.deleteSelfPermission(userPermission.id!)
+  if (!userPermission) {
+    return
   }
+  await PermissionApi.deleteSelfPermission(userPermission.id!)
+  message.success('退出团队成员成功!')
+  emits('quitTeam')
 }
 
 watch(

+ 9 - 14
src/views/mall/product/comment/CommentForm.vue

@@ -1,17 +1,17 @@
 <template>
-  <Dialog title="添加虚拟评论" v-model="dialogVisible">
+  <Dialog v-model="dialogVisible" title="添加虚拟评论">
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
       label-width="100px"
-      v-loading="formLoading"
     >
       <el-form-item label="商品" prop="spuId">
         <SpuShowcase v-model="formData.spuId" :limit="1" />
       </el-form-item>
-      <el-form-item label="商品规格" prop="skuId" v-if="formData.spuId">
-        <div @click="handleSelectSku" class="h-60px w-60px">
+      <el-form-item v-if="formData.spuId" label="商品规格" prop="skuId">
+        <div class="h-60px w-60px" @click="handleSelectSku">
           <div v-if="skuData && skuData.picUrl">
             <el-image :src="skuData.picUrl" />
           </div>
@@ -27,7 +27,7 @@
         <el-input v-model="formData.userNickname" placeholder="请输入用户名称" />
       </el-form-item>
       <el-form-item label="评论内容" prop="content">
-        <el-input type="textarea" v-model="formData.content" />
+        <el-input v-model="formData.content" type="textarea" />
       </el-form-item>
       <el-form-item label="描述星级" prop="descriptionScores">
         <el-rate v-model="formData.descriptionScores" />
@@ -40,13 +40,13 @@
       </el-form-item>
     </el-form>
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
-  <SkuTableSelect ref="skuTableSelectRef" @change="handleSkuChange" :spu-id="formData.spuId" />
+  <SkuTableSelect ref="skuTableSelectRef" :spu-id="formData.spuId" @change="handleSkuChange" />
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import * as CommentApi from '@/api/mall/product/comment'
 import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
 import * as ProductSpuApi from '@/api/mall/product/spu'
@@ -115,13 +115,8 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    //处理评论图片
-    const picUrls = formData.value.picUrls.map((item) => {
-      return item?.url ? item.url : item
-    })
-    const data = { ...formData.value, picUrls }
     if (formType.value === 'create') {
-      await CommentApi.createComment(data)
+      await CommentApi.createComment(unref(formData.value) as any)
       message.success(t('common.createSuccess'))
     }
     dialogVisible.value = false

+ 15 - 19
src/views/mall/product/spu/form/InfoForm.vue

@@ -1,16 +1,16 @@
 <!-- 商品发布 - 基础设置 -->
 <template>
-  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+  <el-form ref="formRef" :disabled="isDetail" :model="formData" :rules="rules" label-width="120px">
     <el-form-item label="商品名称" prop="name">
       <el-input
         v-model="formData.name"
-        placeholder="请输入商品名称"
-        type="textarea"
         :autosize="{ minRows: 2, maxRows: 2 }"
-        maxlength="64"
-        :show-word-limit="true"
         :clearable="true"
+        :show-word-limit="true"
         class="w-80!"
+        maxlength="64"
+        placeholder="请输入商品名称"
+        type="textarea"
       />
     </el-form-item>
     <el-form-item label="商品分类" prop="categoryId">
@@ -20,12 +20,12 @@
         :props="defaultProps"
         class="w-80"
         clearable
-        placeholder="请选择商品分类"
         filterable
+        placeholder="请选择商品分类"
       />
     </el-form-item>
     <el-form-item label="商品品牌" prop="brandId">
-      <el-select v-model="formData.brandId" placeholder="请选择商品品牌" class="w-80">
+      <el-select v-model="formData.brandId" class="w-80" placeholder="请选择商品品牌">
         <el-option
           v-for="item in brandList"
           :key="item.id"
@@ -35,25 +35,25 @@
       </el-select>
     </el-form-item>
     <el-form-item label="商品关键字" prop="keyword">
-      <el-input v-model="formData.keyword" placeholder="请输入商品关键字" class="w-80!" />
+      <el-input v-model="formData.keyword" class="w-80!" placeholder="请输入商品关键字" />
     </el-form-item>
     <el-form-item label="商品简介" prop="introduction">
       <el-input
         v-model="formData.introduction"
-        placeholder="请输入商品名称"
-        type="textarea"
         :autosize="{ minRows: 2, maxRows: 2 }"
-        maxlength="128"
-        :show-word-limit="true"
         :clearable="true"
+        :show-word-limit="true"
         class="w-80!"
+        maxlength="128"
+        placeholder="请输入商品名称"
+        type="textarea"
       />
     </el-form-item>
     <el-form-item label="商品封面图" prop="picUrl">
-      <UploadImg v-model="formData.picUrl" height="80px" :disabled="isDetail" />
+      <UploadImg v-model="formData.picUrl" :disabled="isDetail" height="80px" />
     </el-form-item>
     <el-form-item label="商品轮播图" prop="sliderPicUrls">
-      <UploadImgs v-model:modelValue="formData.sliderPicUrls" :disabled="isDetail" />
+      <UploadImgs v-model="formData.sliderPicUrls" :disabled="isDetail" />
     </el-form-item>
   </el-form>
 </template>
@@ -64,9 +64,9 @@ import { propTypes } from '@/utils/propTypes'
 import { defaultProps, handleTree } from '@/utils/tree'
 import type { Spu } from '@/api/mall/product/spu'
 import * as ProductCategoryApi from '@/api/mall/product/category'
+import { CategoryVO } from '@/api/mall/product/category'
 import * as ProductBrandApi from '@/api/mall/product/brand'
 import { BrandVO } from '@/api/mall/product/brand'
-import { CategoryVO } from '@/api/mall/product/category'
 
 defineOptions({ name: 'ProductSpuInfoForm' })
 const props = defineProps({
@@ -107,10 +107,6 @@ watch(
       return
     }
     copyValueToTarget(formData, data)
-    // TODO @puhui999:优化多文件上传,看看有没可能搞成返回 v-model 图片列表这种
-    formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
-      url: item
-    }))
   },
   {
     immediate: true

+ 6 - 16
src/views/mall/promotion/diy/page/DiyPageForm.vue

@@ -1,11 +1,11 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
       label-width="100px"
-      v-loading="formLoading"
     >
       <el-form-item label="页面名称" prop="name">
         <el-input v-model="formData.name" placeholder="请输入页面名称" />
@@ -18,12 +18,12 @@
       </el-form-item>
     </el-form>
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import * as DiyPageApi from '@/api/mall/promotion/diy/page'
 
 /** 装修页面表单 */
@@ -57,13 +57,7 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      const diyPage = await DiyPageApi.getDiyPage(id) // 处理预览图
-      if (diyPage?.previewPicUrls?.length > 0) {
-        diyPage.previewPicUrls = diyPage.previewPicUrls.map((url: string) => {
-          return { url }
-        })
-      }
-      formData.value = diyPage
+      formData.value = await DiyPageApi.getDiyPage(id)
     } finally {
       formLoading.value = false
     }
@@ -81,11 +75,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    // 处理预览图
-    const previewPicUrls = formData.value.previewPicUrls.map((item) => {
-      return item['url'] ? item['url'] : item
-    })
-    const data = { ...formData.value, previewPicUrls } as unknown as DiyPageApi.DiyPageVO
+    const data = formData.value as unknown as DiyPageApi.DiyPageVO
     if (formType.value === 'create') {
       await DiyPageApi.createDiyPage(data)
       message.success(t('common.createSuccess'))

+ 6 - 17
src/views/mall/promotion/diy/template/DiyTemplateForm.vue

@@ -1,11 +1,11 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
       label-width="100px"
-      v-loading="formLoading"
     >
       <el-form-item label="模板名称" prop="name">
         <el-input v-model="formData.name" placeholder="请输入模板名称" />
@@ -18,12 +18,12 @@
       </el-form-item>
     </el-form>
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
 
 /** 装修模板表单 */
@@ -57,14 +57,7 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      const diyTemplate = await DiyTemplateApi.getDiyTemplate(id)
-      // 处理预览图
-      if (diyTemplate?.previewPicUrls?.length > 0) {
-        diyTemplate.previewPicUrls = diyTemplate.previewPicUrls.map((url: string) => {
-          return { url }
-        })
-      }
-      formData.value = diyTemplate
+      formData.value = await DiyTemplateApi.getDiyTemplate(id)
     } finally {
       formLoading.value = false
     }
@@ -82,11 +75,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    // 处理预览图
-    const previewPicUrls = formData.value.previewPicUrls.map((item) => {
-      return item['url'] ? item['url'] : item
-    })
-    const data = { ...formData.value, previewPicUrls } as unknown as DiyTemplateApi.DiyTemplateVO
+    const data = formData.value as unknown as DiyTemplateApi.DiyTemplateVO
     if (formType.value === 'create') {
       await DiyTemplateApi.createDiyTemplate(data)
       message.success(t('common.createSuccess'))

+ 28 - 33
src/views/mall/trade/config/index.vue

@@ -2,12 +2,12 @@
   <ContentWrap>
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
       label-width="120px"
-      v-loading="formLoading"
     >
-      <el-form-item label="hideId" v-show="false">
+      <el-form-item v-show="false" label="hideId">
         <el-input v-model="formData.id" />
       </el-form-item>
       <el-tabs>
@@ -16,9 +16,9 @@
           <el-form-item label="退款理由" prop="afterSaleRefundReasons">
             <el-select
               v-model="formData.afterSaleRefundReasons"
+              allow-create
               filterable
               multiple
-              allow-create
               placeholder="请直接输入退款理由"
             >
               <el-option
@@ -50,15 +50,15 @@
         <el-tab-pane label="配送">
           <el-form-item label="启用包邮" prop="deliveryExpressFreeEnabled">
             <el-switch v-model="formData.deliveryExpressFreeEnabled" style="user-select: none" />
-            <el-text class="w-full" size="small" type="info"> 商城是否启用全场包邮 </el-text>
+            <el-text class="w-full" size="small" type="info"> 商城是否启用全场包邮</el-text>
           </el-form-item>
           <el-form-item label="满额包邮" prop="deliveryExpressFreePrice">
             <el-input-number
               v-model="formData.deliveryExpressFreePrice"
-              placeholder="请输入满额包邮"
-              class="!w-xs"
-              :precision="2"
               :min="0"
+              :precision="2"
+              class="!w-xs"
+              placeholder="请输入满额包邮"
             />
             <el-text class="w-full" size="small" type="info">
               商城商品满多少金额即可包邮,单位:元
@@ -72,7 +72,7 @@
         <el-tab-pane label="分销">
           <el-form-item label="分佣启用" prop="brokerageEnabled">
             <el-switch v-model="formData.brokerageEnabled" style="user-select: none" />
-            <el-text class="w-full" size="small" type="info"> 商城是否开启分销模式 </el-text>
+            <el-text class="w-full" size="small" type="info"> 商城是否开启分销模式</el-text>
           </el-form-item>
           <el-form-item label="分佣模式" prop="brokerageEnabledCondition">
             <el-radio-group v-model="formData.brokerageEnabledCondition">
@@ -109,7 +109,7 @@
             </el-text>
           </el-form-item>
           <el-form-item label="分销海报图">
-            <UploadImgs v-model="formData.brokeragePosterUrls" width="75px" height="125px" />
+            <UploadImgs v-model="formData.brokeragePosterUrls" height="125px" width="75px" />
             <el-text class="w-full" size="small" type="info">
               个人中心分销海报图片,建议尺寸 600x1000
             </el-text>
@@ -117,10 +117,10 @@
           <el-form-item label="一级返佣比例" prop="brokerageFirstPercent">
             <el-input-number
               v-model="formData.brokerageFirstPercent"
-              placeholder="请输入一级返佣比例"
-              class="!w-xs"
-              :min="0"
               :max="100"
+              :min="0"
+              class="!w-xs"
+              placeholder="请输入一级返佣比例"
             />
             <el-text class="w-full" size="small" type="info">
               订单交易成功后给推广人返佣的百分比
@@ -129,10 +129,10 @@
           <el-form-item label="二级返佣比例" prop="brokerageSecondPercent">
             <el-input-number
               v-model="formData.brokerageSecondPercent"
-              placeholder="请输入二级返佣比例"
-              class="!w-xs"
-              :min="0"
               :max="100"
+              :min="0"
+              class="!w-xs"
+              placeholder="请输入二级返佣比例"
             />
             <el-text class="w-full" size="small" type="info">
               订单交易成功后给推广人的推荐人返佣的百分比
@@ -141,9 +141,9 @@
           <el-form-item label="佣金冻结天数" prop="brokerageFrozenDays">
             <el-input-number
               v-model="formData.brokerageFrozenDays"
-              placeholder="请输入佣金冻结天数"
-              class="!w-xs"
               :min="0"
+              class="!w-xs"
+              placeholder="请输入佣金冻结天数"
             />
             <el-text class="w-full" size="small" type="info">
               防止用户退款,佣金被提现了,所以需要设置佣金冻结时间,单位:天
@@ -152,10 +152,10 @@
           <el-form-item label="提现最低金额" prop="brokerageWithdrawMinPrice">
             <el-input-number
               v-model="formData.brokerageWithdrawMinPrice"
-              placeholder="请输入提现最低金额"
-              class="!w-xs"
-              :precision="2"
               :min="0"
+              :precision="2"
+              class="!w-xs"
+              placeholder="请输入提现最低金额"
             />
             <el-text class="w-full" size="small" type="info">
               用户提现最低金额限制,单位:元
@@ -164,10 +164,10 @@
           <el-form-item label="提现手续费" prop="brokerageWithdrawFeePercent">
             <el-input-number
               v-model="formData.brokerageWithdrawFeePercent"
-              placeholder="请输入提现手续费"
-              class="!w-xs"
-              :min="0"
               :max="100"
+              :min="0"
+              class="!w-xs"
+              placeholder="请输入提现手续费"
             />
             <el-text class="w-full" size="small" type="info">
               提现手续费百分比,范围 0-100,0 为无提现手续费。例:设置 10,即收取 10% 手续费,提现
@@ -184,21 +184,22 @@
                 {{ dict.label }}
               </el-checkbox>
             </el-checkbox-group>
-            <el-text class="w-full" size="small" type="info"> 商城开通提现的付款方式 </el-text>
+            <el-text class="w-full" size="small" type="info"> 商城开通提现的付款方式</el-text>
           </el-form-item>
         </el-tab-pane>
       </el-tabs>
       <!-- 保存 -->
       <el-form-item>
-        <el-button type="primary" @click="submitForm" :loading="formLoading"> 保存 </el-button>
+        <el-button :loading="formLoading" type="primary" @click="submitForm"> 保存</el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
 </template>
 
-<script setup lang="ts">
+<script lang="ts" setup>
 import * as ConfigApi from '@/api/mall/trade/config'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { cloneDeep } from 'lodash-es'
 
 defineOptions({ name: 'TradeConfig' })
 
@@ -253,12 +254,7 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = {
-      ...formData.value
-    } as unknown as ConfigApi.ConfigVO
-    data.brokeragePosterUrls = formData.value.brokeragePosterUrls.map((item: any) => {
-      return item?.url ? item.url : item
-    })
+    const data = cloneDeep(unref(formData.value)) as unknown as ConfigApi.ConfigVO
     // 金额放大
     data.deliveryExpressFreePrice = data.deliveryExpressFreePrice * 100
     data.brokerageWithdrawMinPrice = data.brokerageWithdrawMinPrice * 100
@@ -275,7 +271,6 @@ const getConfig = async () => {
   try {
     const data = await ConfigApi.getTradeConfig()
     if (data != null) {
-      data.brokeragePosterUrls = data.brokeragePosterUrls.map((url) => ({ url }))
       formData.value = data
       // 金额缩小
       formData.value.deliveryExpressFreePrice = data.deliveryExpressFreePrice / 100