Browse Source

Merge remote-tracking branch 'origin/dev' into dev

owen 1 năm trước cách đây
mục cha
commit
bdf95dc0f8

+ 1 - 0
.env.dev

@@ -5,6 +5,7 @@ VITE_DEV=true
 
 # 请求路径
 VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
+# VITE_BASE_URL='http://dofast.demo.huizhizao.vip:20001'
 
 # 上传路径
 VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'

+ 5 - 0
src/api/crm/contact/index.ts

@@ -80,3 +80,8 @@ export const createContactBusinessList = async (data: ContactBusinessReqVO) => {
 export const deleteContactBusinessList = async (data: ContactBusinessReqVO) => {
   return await request.delete({ url: `/crm/contact/delete-business-list`, data })
 }
+
+// 查询联系人操作日志
+export const getOperateLogPage = async (params: any) => {
+  return await request.get({ url: '/crm/contact/operate-log-page', params })
+}

+ 15 - 2
src/api/crm/customer/index.ts

@@ -69,11 +69,24 @@ export const queryAllList = async () => {
 }
 
 // 查询客户操作日志
-export const getOperateLogPage = async (params: any) => {
-  return await request.get({ url: '/crm/customer/operate-log-page', params })
+export const getOperateLogPage = async (id: number) => {
+  return await request.get({ url: '/crm/customer/operate-log-page?id=' + id })
 }
 
+// ======================= 业务操作 =======================
+
 // 锁定/解锁客户
 export const lockCustomer = async (id: number, lockStatus: boolean) => {
   return await request.put({ url: `/crm/customer/lock`, data: { id, lockStatus } })
 }
+
+// TODO @puhui999:方法名,改成和后端一致哈
+// 领取公海客户
+export const receive = async (ids: any[]) => {
+  return await request.put({ url: '/crm/customer/receive', params: { ids: ids.join(',') } })
+}
+
+// 客户放入公海
+export const putPool = async (id: number) => {
+  return await request.put({ url: `/crm/customer/put-pool?id=${id}` })
+}

+ 0 - 12
src/api/crm/permission/index.ts

@@ -58,15 +58,3 @@ export const deletePermissionBatch = async (params) => {
 export const deleteSelfPermission = async (id) => {
   return await request.delete({ url: '/crm/permission/quit-team?id=' + id })
 }
-
-// TODO @puhui999:调整下位置
-// 领取公海数据
-export const receive = async (data: { bizType: number; bizId: number }) => {
-  return await request.put({ url: `/crm/permission/receive`, data })
-}
-
-// TODO @puhui999:调整下位置
-// 数据放入公海
-export const putPool = async (data: { bizType: number; bizId: number }) => {
-  return await request.put({ url: `/crm/permission/put-pool`, data })
-}

+ 48 - 0
src/api/mall/promotion/reward/rewardActivity.ts

@@ -0,0 +1,48 @@
+import request from '@/config/axios'
+
+export interface DiscountActivityVO {
+  id?: number
+  name?: string
+  startTime?: Date
+  endTime?: Date
+  remark?: string
+  conditionType?: number
+  productScope?: number
+  productSpuIds?: number[]
+  rules?: DiscountProductVO[]
+}
+
+// 优惠规则
+export interface DiscountProductVO {
+  limit: number
+  discountPrice: number
+  freeDelivery: boolean
+  point: number
+  couponIds: number[]
+  couponCounts: number[]
+}
+
+// 新增满减送活动
+export const createRewardActivity = async (data: DiscountActivityVO) => {
+  return await request.post({ url: '/promotion/reward-activity/create', data })
+}
+
+// 更新满减送活动
+export const updateRewardActivity = async (data: DiscountActivityVO) => {
+  return await request.put({ url: '/promotion/reward-activity/update', data })
+}
+
+// 查询满减送活动列表
+export const getRewardActivityPage = async (params) => {
+  return await request.get({ url: '/promotion/reward-activity/page', params })
+}
+
+// 查询满减送活动详情
+export const getReward = async (id: number) => {
+  return await request.get({ url: '/promotion/reward-activity/get?id=' + id })
+}
+
+// 删除限时折扣活动
+export const deleteRewardActivity = async (id: number) => {
+  return await request.delete({ url: '/promotion/reward-activity/delete?id=' + id })
+}

+ 0 - 1
src/api/mall/trade/config/index.ts

@@ -8,7 +8,6 @@ export interface ConfigVO {
   brokerageFirstPercent: number
   brokerageSecondPercent: number
   brokerageWithdrawMinPrice: number
-  brokerageBankNames: string
   brokerageFrozenDays: number
   brokerageWithdrawTypes: string
 }

+ 0 - 4
src/components/AppLinkInput/data.ts

@@ -181,10 +181,6 @@ export const APP_LINK_GROUP_LIST = [
       {
         name: '充值记录',
         path: '/pages/pay/recharge-log'
-      },
-      {
-        name: '申请提现',
-        path: '/pages/pay/withdraw'
       }
     ]
   },

+ 9 - 79
src/components/OperateLogV2/src/OperateLogV2.vue

@@ -1,37 +1,16 @@
 <template>
+  <!-- TODO @puhui999:左边不用有空隙哈 -->
   <div class="p-20px">
     <el-timeline>
       <el-timeline-item
-        v-for="(log, index) in logDataList"
+        v-for="(log, index) in logList"
         :key="index"
         :timestamp="formatDate(log.createTime)"
         placement="top"
       >
         <div class="el-timeline-right-content">
-          <el-row>
-            <el-col :span="24" class="mb-10px">
-              =======================
-              <el-tag class="mr-10px" type="success">{{ log.userName }}</el-tag>
-              <span>{{ log.title }}</span>
-              =======================
-            </el-col>
-            <!-- 先处理一下有几行-->
-            <template v-for="colNum in log.colSize" :key="colNum + 'col'">
-              <el-col :span="24" class="mb-10px">
-                <!-- 处理每一行-->
-                <template
-                  v-for="(tagVal, index2) in log.tagsContentList.slice(
-                    (colNum - 1) * 3,
-                    3 * colNum
-                  )"
-                  :key="index2"
-                >
-                  <el-tag class="mx-10px"> {{ tagVal }}</el-tag>
-                  <span>{{ log.contentStrList[index2] }}</span>
-                </template>
-              </el-col>
-            </template>
-          </el-row>
+          <el-tag class="mr-10px" type="success">{{ log.userName }}</el-tag>
+          {{ log.action }}
         </div>
         <template #dot>
           <span :style="{ backgroundColor: getUserTypeColor(log.userType) }" class="dot-node-style">
@@ -51,11 +30,13 @@ import { ElTag } from 'element-plus'
 
 defineOptions({ name: 'OperateLogV2' })
 
-const props = defineProps<{
+interface Props {
   logList: OperateLogV2VO[] // 操作日志列表
-}>()
+}
 
-const logDataList = ref<OperateLogV2VO[]>([]) // 操作日志列表
+withDefaults(defineProps<Props>(), {
+  logList: () => []
+})
 
 /** 获得 userType 颜色 */
 const getUserTypeColor = (type: number) => {
@@ -72,57 +53,6 @@ const getUserTypeColor = (type: number) => {
   }
   return '#409EFF'
 }
-
-// 提取 tag 所需内容和位置
-const renderTags = (content: string) => {
-  let newStr = unref(content).slice() // 去掉引用
-  newStr = newStr.replaceAll('【】', '【空】').replaceAll(';', '') // 处理掉分号 特殊:处理一下空的情况
-  const regex = /【([^【】]+)】/g
-  const fg = '|' // 原始位置替换符号
-  let match: any[] | null
-  let matchStr: string[] = []
-  let oldStr: string[] = []
-  while ((match = regex.exec(newStr)) !== null) {
-    matchStr.push(match[1]) // 提取值
-    oldStr.push(match[0]) // 原值
-  }
-  // 为什么重新循环不放在 while 中一起是因为替换重新赋值过后 match 值就不准确了
-  oldStr.forEach((item) => {
-    newStr = newStr.replace(item, fg)
-  })
-  return [newStr.split(fg), matchStr]
-}
-
-const initLog = () => {
-  logDataList.value = props.logList.map((logItem) => {
-    const keyValue = renderTags(logItem.action)
-    // 挂载数据
-    logItem.contentStrList = keyValue[0]
-    if (keyValue[0][0] === '从') {
-      logItem.title = logItem.subType
-    } else {
-      logItem.title = keyValue[0][0]
-      logItem.contentStrList.splice(0, 1)
-    }
-    logItem.colSize = keyValue[0].length / 3 // 变更记录行数
-    logItem.tagsContentList = keyValue[1]
-    return logItem
-  })
-}
-
-watch(
-  () => props.logList.length,
-  (newObj) => {
-    if (newObj) {
-      initLog()
-      console.log(logDataList.value)
-    }
-  },
-  {
-    immediate: true,
-    deep: true
-  }
-)
 </script>
 
 <style lang="scss" scoped>

+ 4 - 1
src/components/UploadFile/src/UploadFile.vue

@@ -100,7 +100,9 @@ const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
 // 文件上传成功
 const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
   message.success('上传成功')
-  fileList.value.shift()
+  // 删除自身
+  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)
@@ -144,6 +146,7 @@ watch(
       fileList.value.push(
         ...val.split(',').map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
       )
+      return
     }
     // 情况2:数组
     fileList.value.push(

+ 3 - 3
src/utils/constants.ts

@@ -244,15 +244,15 @@ export const CouponTemplateTakeTypeEnum = {
  */
 export const PromotionProductScopeEnum = {
   ALL: {
-    scope: 1,
+    scope: 10,
     name: '通用劵'
   },
   SPU: {
-    scope: 2,
+    scope: 20,
     name: '商品劵'
   },
   CATEGORY: {
-    scope: 3,
+    scope: 30,
     name: '品类劵'
   }
 }

+ 4 - 4
src/utils/index.ts

@@ -194,10 +194,10 @@ export const copyValueToTarget = (target, source) => {
  * 将一个整数转换为分数保留两位小数
  * @param num
  */
-export const formatToFraction = (num: number | string | undefined): number => {
-  if (typeof num === 'undefined') return 0
+export const formatToFraction = (num: number | string | undefined): string => {
+  if (typeof num === 'undefined') return '0.00'
   const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
-  return parseFloat((parsedNumber / 100).toFixed(2))
+  return (parsedNumber / 100.0).toFixed(2)
 }
 
 /**
@@ -249,7 +249,7 @@ export const yuanToFen = (amount: string | number): number => {
 /**
  * 分转元
  */
-export const fenToYuan = (price: string | number): number => {
+export const fenToYuan = (price: string | number): string => {
   return formatToFraction(price)
 }
 

+ 1 - 1
src/views/Login/Login.vue

@@ -1,7 +1,7 @@
 <template>
   <div
     :class="prefixCls"
-    class="relative h-[100%] lt-xl:bg-[var(--login-bg-color)] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px"
+    class="relative h-[100%] lt-xl:px-10px lt-md:px-10px lt-sm:px-10px lt-xl:px-10px"
   >
     <div class="relative mx-auto h-full flex">
       <div

+ 3 - 2
src/views/crm/contact/ContactForm.vue

@@ -136,7 +136,6 @@
           </el-form-item>
         </el-col>
       </el-row>
-      <!-- TODO @zyna:解决下 ide 报错 -->
       <el-row>
         <el-col :span="12">
           <el-form-item label="直属上级" prop="parentId">
@@ -233,7 +232,8 @@ const ownerUserList = ref<any[]>([])
 const userList = ref<UserApi.UserVO[]>([]) // 用户列表
 // TODO 芋艿:统一的客户选择面板
 const customerList = ref<CustomerApi.CustomerVO[]>([]) // 客户列表
-const allContactList = ref([]) // 所有联系人列表
+const allContactList = ref<ContactApi.ContactVO[]>([]) // 所有联系人列表
+
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
@@ -255,6 +255,7 @@ const open = async (type: string, id?: number) => {
   }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
 /** 提交表单 */
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {

+ 67 - 65
src/views/crm/contact/detail/ContactDetailsInfo.vue

@@ -1,74 +1,76 @@
 <template>
-  <!-- TODO @zyna:少了一个外边框 -->
-  <el-collapse v-model="activeNames">
-    <el-collapse-item name="basicInfo">
-      <template #title>
-        <span class="text-base font-bold">基本信息</span>
-      </template>
-      <el-descriptions :column="4">
-        <el-descriptions-item label="姓名">
-          {{ contact.name }}
-        </el-descriptions-item>
-        <el-descriptions-item label="客户">
-          {{ contact.customerName }}
-        </el-descriptions-item>
-        <el-descriptions-item label="手机">
-          {{ contact.mobile }}
-        </el-descriptions-item>
-        <el-descriptions-item label="座机">
-          {{ contact.telephone }}
-        </el-descriptions-item>
-        <el-descriptions-item label="邮箱">
-          {{ contact.email }}
-        </el-descriptions-item>
-        <el-descriptions-item label="QQ">
-          {{ contact.qq }}
-        </el-descriptions-item>
-        <el-descriptions-item label="微信">
-          {{ contact.wechat }}
-        </el-descriptions-item>
-        <el-descriptions-item label="下次联系时间">
-          {{ contact.nextTime ? formatDate(contact.nextTime) : '空' }}
-        </el-descriptions-item>
-        <el-descriptions-item label="所在地">
-          {{ contact.areaName }}
-        </el-descriptions-item>
-        <el-descriptions-item label="详细地址">
-          {{ contact.address }}
-        </el-descriptions-item>
-        <el-descriptions-item label="性别">
-          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="contact.sex" />
-        </el-descriptions-item>
-        <el-descriptions-item label="备注">
-          {{ contact.remark }}
-        </el-descriptions-item>
-      </el-descriptions>
-    </el-collapse-item>
-    <el-collapse-item name="systemInfo">
-      <template #title>
-        <span class="text-base font-bold">系统信息</span>
-      </template>
-      <el-descriptions :column="2">
-        <el-descriptions-item label="负责人">
-          {{ contact.ownerUserName }}
-        </el-descriptions-item>
-        <el-descriptions-item label="创建人">
-          {{ contact.creatorName }}
-        </el-descriptions-item>
-        <el-descriptions-item label="创建时间">
-          {{ contact.createTime ? formatDate(contact.createTime) : '空' }}
-        </el-descriptions-item>
-        <el-descriptions-item label="更新时间">
-          {{ contact.updateTime ? formatDate(contact.updateTime) : '空' }}
-        </el-descriptions-item>
-      </el-descriptions>
-    </el-collapse-item>
-  </el-collapse>
+  <ContentWrap>
+    <el-collapse v-model="activeNames">
+      <el-collapse-item name="basicInfo">
+        <template #title>
+          <span class="text-base font-bold">基本信息</span>
+        </template>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="姓名">
+            {{ contact.name }}
+          </el-descriptions-item>
+          <el-descriptions-item label="客户">
+            {{ contact.customerName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="手机">
+            {{ contact.mobile }}
+          </el-descriptions-item>
+          <el-descriptions-item label="座机">
+            {{ contact.telephone }}
+          </el-descriptions-item>
+          <el-descriptions-item label="邮箱">
+            {{ contact.email }}
+          </el-descriptions-item>
+          <el-descriptions-item label="QQ">
+            {{ contact.qq }}
+          </el-descriptions-item>
+          <el-descriptions-item label="微信">
+            {{ contact.wechat }}
+          </el-descriptions-item>
+          <el-descriptions-item label="下次联系时间">
+            {{ contact.nextTime ? formatDate(contact.nextTime) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="所在地">
+            {{ contact.areaName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="详细地址">
+            {{ contact.detailAddress }}
+          </el-descriptions-item>
+          <el-descriptions-item label="性别">
+            <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="contact.sex" />
+          </el-descriptions-item>
+          <el-descriptions-item label="备注">
+            {{ contact.remark }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-collapse-item>
+      <el-collapse-item name="systemInfo">
+        <template #title>
+          <span class="text-base font-bold">系统信息</span>
+        </template>
+        <el-descriptions :column="2">
+          <el-descriptions-item label="负责人">
+            {{ contact.ownerUserName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="创建人">
+            {{ contact.creatorName }}
+          </el-descriptions-item>
+          <el-descriptions-item label="创建时间">
+            {{ contact.createTime ? formatDate(contact.createTime) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="更新时间">
+            {{ contact.updateTime ? formatDate(contact.updateTime) : '空' }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-collapse-item>
+    </el-collapse>
+  </ContentWrap>
 </template>
 <script setup lang="ts">
 import * as ContactApi from '@/api/crm/contact'
 import { DICT_TYPE } from '@/utils/dict'
 import { formatDate } from '@/utils/formatTime'
+
 const { contact } = defineProps<{
   contact: ContactApi.ContactVO
 }>()

+ 19 - 2
src/views/crm/contact/detail/index.vue

@@ -5,7 +5,9 @@
       <el-tab-pane label="详细资料">
         <ContactDetailsInfo :contact="contact" />
       </el-tab-pane>
-      <el-tab-pane label="操作日志" lazy>TODO 待开发</el-tab-pane>
+      <el-tab-pane label="操作日志">
+        <OperateLogV2 :log-list="logList" />
+      </el-tab-pane>
       <el-tab-pane label="团队成员" lazy>
         <PermissionList :biz-id="contact.id!" :biz-type="BizTypeEnum.CRM_CONTACT" />
       </el-tab-pane>
@@ -20,7 +22,6 @@
   </el-col>
 </template>
 <script setup lang="ts">
-import { ElMessage } from 'element-plus' // TODO @zyna:使用 hook 引入 message
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import * as ContactApi from '@/api/crm/contact'
 import ContactDetailsHeader from '@/views/crm/contact/detail/ContactDetailsHeader.vue'
@@ -28,6 +29,7 @@ import ContactDetailsInfo from '@/views/crm/contact/detail/ContactDetailsInfo.vu
 import BusinessList from '@/views/crm/business/components/BusinessList.vue' // 商机列表
 import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
 import { BizTypeEnum } from '@/api/crm/permission'
+import { OperateLogV2VO } from '@/api/system/operatelog'
 
 defineOptions({ name: 'CrmContactDetail' })
 
@@ -41,11 +43,26 @@ const getContactData = async (id: number) => {
   loading.value = true
   try {
     contact.value = await ContactApi.getContact(id)
+    await getOperateLog(id)
   } finally {
     loading.value = false
   }
 }
 
+/**
+ * 获取操作日志
+ */
+const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
+const getOperateLog = async (contactId: number) => {
+  if (!contactId) {
+    return
+  }
+  const data = await ContactApi.getOperateLogPage({
+    bizId: contactId
+  })
+  logList.value = data.list
+}
+
 /** 初始化 */
 const { delView } = useTagsViewStore() // 视图操作
 const { currentRoute } = useRouter() // 路由

+ 2 - 43
src/views/crm/customer/detail/CustomerDetailsHeader.vue

@@ -11,19 +11,7 @@
       </div>
       <div>
         <!-- 右上:按钮 -->
-        <el-button
-          type="primary"
-          v-hasPermi="['crm:customer:update']"
-          @click="openForm(customer.id)"
-        >
-          编辑
-        </el-button>
-        <!-- TODO @puhui999:转移的操作接入 -->
-        <el-button type="primary" @click="transfer">转移</el-button>
-        <!-- TODO @puhui999:修改成交状态的接入 -->
-        <el-button>更改成交状态</el-button>
-        <el-button v-if="customer.lockStatus" @click="handleUnlock(customer.id!)">解锁</el-button>
-        <el-button v-else @click="handleLock(customer.id!)">锁定</el-button>
+        <slot></slot>
       </div>
     </div>
   </div>
@@ -42,43 +30,14 @@
       <el-descriptions-item label="首要联系人电话">{{ customer.mobile }}</el-descriptions-item>
     </el-descriptions>
   </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <CustomerForm ref="formRef" @success="emit('refresh')" />
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE } from '@/utils/dict'
 import * as CustomerApi from '@/api/crm/customer'
-import CustomerForm from '../CustomerForm.vue'
 
 defineOptions({ name: 'CustomerDetailsHeader' })
-
-const { customer, loading } = defineProps<{
+defineProps<{
   customer: CustomerApi.CustomerVO // 客户信息
   loading: boolean // 加载中
 }>()
-const message = useMessage() // 消息弹窗
-
-/** 修改操作 */
-const formRef = ref()
-const openForm = (id?: number) => {
-  formRef.value.open('update', id)
-}
-
-/** 锁定操作 */
-const handleLock = async (id: number) => {
-  await CustomerApi.lockCustomer(id, true)
-  message.success('锁定成功')
-  emit('refresh')
-}
-
-/** 解锁操作 */
-const handleUnlock = async (id: number) => {
-  console.log(customer, '=======')
-  await CustomerApi.lockCustomer(id, false)
-  message.success('解锁成功')
-  emit('refresh')
-}
-
-const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
 </script>

+ 89 - 23
src/views/crm/customer/detail/index.vue

@@ -1,5 +1,20 @@
 <template>
-  <CustomerDetailsHeader :customer="customer" :loading="loading" @refresh="getCustomer(id)" />
+  <CustomerDetailsHeader :customer="customer" :loading="loading">
+    <!-- @puhui999:返回是不是可以去掉哈,貌似用途可能不大 -->
+    <el-button @click="close">返回</el-button>
+    <!-- TODO puhui999: 按钮数据权限收尾统一完善,需要按权限分级和客户状态来动态显示匹配的按钮 -->
+    <el-button v-hasPermi="['crm:customer:update']" type="primary" @click="openForm">
+      编辑
+    </el-button>
+    <!-- TODO @puhui999:转移的操作接入 -->
+    <el-button type="primary" @click="transfer">转移</el-button>
+    <!-- TODO @puhui999:修改成交状态的接入 -->
+    <el-button>更改成交状态</el-button>
+    <el-button v-if="customer.lockStatus" @click="handleUnlock">解锁</el-button>
+    <el-button v-if="!customer.lockStatus" @click="handleLock">锁定</el-button>
+    <el-button v-if="!customer.ownerUserId" type="primary" @click="receive">领取客户</el-button>
+    <el-button v-if="customer.ownerUserId" @click="putPool">客户放入公海</el-button>
+  </CustomerDetailsHeader>
   <el-col>
     <el-tabs>
       <el-tab-pane label="详细资料">
@@ -11,7 +26,7 @@
       <el-tab-pane label="联系人" lazy>
         <ContactList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
       </el-tab-pane>
-      <el-tab-pane label="团队成员" lazy>
+      <el-tab-pane label="团队成员">
         <PermissionList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
       </el-tab-pane>
       <el-tab-pane label="商机" lazy>
@@ -27,10 +42,14 @@
       <el-tab-pane label="回访" lazy>TODO 待开发</el-tab-pane>
     </el-tabs>
   </el-col>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <CustomerForm ref="formRef" @success="getCustomer" />
 </template>
 <script lang="ts" setup>
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import * as CustomerApi from '@/api/crm/customer'
+import CustomerForm from '@/views/crm/customer/CustomerForm.vue'
 import CustomerDetailsInfo from './CustomerDetailsInfo.vue' // 客户明细 - 详细信息
 import CustomerDetailsHeader from './CustomerDetailsHeader.vue' // 客户明细 - 头部
 import ContactList from '@/views/crm/contact/components/ContactList.vue' // 联系人列表
@@ -40,48 +59,95 @@ import ReceivableList from '@/views/crm/receivable/components/ReceivableList.vue
 import ReceivablePlanList from '@/views/crm/receivable/plan/components/ReceivablePlanList.vue' // 回款计划列表
 import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 团队成员列表(权限)
 import { BizTypeEnum } from '@/api/crm/permission'
-import { OperateLogV2VO } from '@/api/system/operatelog'
+import type { OperateLogV2VO } from '@/api/system/operatelog'
 
 defineOptions({ name: 'CrmCustomerDetail' })
 
-const route = useRoute()
-const id = Number(route.params.id) // 客户编号
+const customerId = ref(0) // 客户编号
 const loading = ref(true) // 加载中
+const message = useMessage() // 消息弹窗
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute, push } = useRouter() // 路由
+
 /** 获取详情 */
 const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO) // 客户详情
-const getCustomer = async (id: number) => {
+const getCustomer = async () => {
   loading.value = true
   try {
-    customer.value = await CustomerApi.getCustomer(id)
-    await getOperateLog(id)
+    customer.value = await CustomerApi.getCustomer(customerId.value)
+    await getOperateLog()
   } finally {
     loading.value = false
   }
 }
+
+/** 编辑客户 */
+const formRef = ref<InstanceType<typeof CustomerForm>>() // 客户表单 Ref
+const openForm = () => {
+  formRef.value?.open('update', customerId.value)
+}
+
+/** 客户转移 */
+const transfer = () => {}
+
+/** 锁定客户 */
+const handleLock = async () => {
+  await message.confirm(`确定锁定客户【${customer.value.name}】 吗?`)
+  await CustomerApi.lockCustomer(unref(customerId.value), true)
+  message.success(`锁定客户【${customer.value.name}】成功`)
+  await getCustomer()
+}
+
+/** 解锁客户 */
+const handleUnlock = async () => {
+  await message.confirm(`确定解锁客户【${customer.value.name}】 吗?`)
+  await CustomerApi.lockCustomer(unref(customerId.value), false)
+  message.success(`解锁客户【${customer.value.name}】成功`)
+  await getCustomer()
+}
+
+// TODO @puhui999:下面两个方法的命名,也用 handleXXX 风格哈
+/** 领取客户 */
+const receive = async () => {
+  await message.confirm(`确定领取客户【${customer.value.name}】 吗?`)
+  await CustomerApi.receive([unref(customerId.value)])
+  message.success(`领取客户【${customer.value.name}】成功`)
+  await getCustomer()
+}
+
+/** 客户放入公海 */
+const putPool = async () => {
+  await message.confirm(`确定将客户【${customer.value.name}】放入公海吗?`)
+  await CustomerApi.putPool(unref(customerId.value))
+  message.success(`客户【${customer.value.name}】放入公海成功`)
+  close()
+}
+
+/** 获取操作日志 */
 const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
-/**
- * 获取操作日志
- */
-const getOperateLog = async (customerId: number) => {
-  if (!customerId) {
+const getOperateLog = async () => {
+  if (!customerId.value) {
     return
   }
-  const data = await CustomerApi.getOperateLogPage({
-    pageNo: 1,
-    pageSize: 10,
-    bizId: customerId
-  })
+  const data = await CustomerApi.getOperateLogPage(customerId.value)
   logList.value = data.list
 }
+
+const close = () => {
+  delView(unref(currentRoute))
+  // TODO 先返回到客户列表
+  push({ name: 'CrmCustomer' })
+}
+
 /** 初始化 */
-const { delView } = useTagsViewStore() // 视图操作
-const { currentRoute } = useRouter() // 路由
+const { params } = useRoute()
 onMounted(() => {
-  if (!id) {
+  if (!params.id) {
     ElMessage.warning('参数错误,客户不能为空!')
-    delView(unref(currentRoute))
+    close()
     return
   }
-  getCustomer(id)
+  customerId.value = params.id as unknown as number
+  getCustomer()
 })
 </script>

+ 105 - 14
src/views/crm/customer/index.vue

@@ -72,10 +72,17 @@
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"> <Icon class="mr-5px" icon="ep:search" /> 搜索 </el-button>
-        <el-button @click="resetQuery"> <Icon class="mr-5px" icon="ep:refresh" />重置 </el-button>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery(undefined)">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
         <el-button v-hasPermi="['crm:customer:create']" type="primary" @click="openForm('create')">
-          <Icon class="mr-5px" icon="ep:plus" /> 新增
+          <Icon class="mr-5px" icon="ep:plus" />
+          新增
         </el-button>
         <el-button
           v-hasPermi="['crm:customer:export']"
@@ -84,7 +91,8 @@
           type="success"
           @click="handleExport"
         >
-          <Icon class="mr-5px" icon="ep:download" /> 导出
+          <Icon class="mr-5px" icon="ep:download" />
+          导出
         </el-button>
       </el-form-item>
     </el-form>
@@ -92,11 +100,20 @@
 
   <!-- 列表 -->
   <ContentWrap>
+    <!-- TODO @puhui999:是不是就 3 重呀,我负责的,我参与的,我下属的 -->
+    <el-tabs v-model="activeName" @tab-click="handleClick">
+      <el-tab-pane label="客户列表" name="1" />
+      <el-tab-pane label="我负责的" name="2" />
+      <el-tab-pane label="我关注的" name="3" />
+      <el-tab-pane label="我参与的" name="4" />
+      <el-tab-pane label="下属负责的" name="5" />
+      <el-tab-pane label="客户公海" name="6" />
+    </el-tabs>
     <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
       <el-table-column align="center" label="编号" prop="id" />
       <el-table-column align="center" label="客户名称" prop="name" width="160">
         <template #default="scope">
-          <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+          <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
             {{ scope.row.name }}
           </el-link>
         </template>
@@ -197,6 +214,7 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as CustomerApi from '@/api/crm/customer'
 import CustomerForm from './CustomerForm.vue'
+import { TabsPaneContext } from 'element-plus'
 
 defineOptions({ name: 'CrmCustomer' })
 
@@ -206,24 +224,78 @@ const { t } = useI18n() // 国际化
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
-const queryParams = reactive({
+const queryParams = ref<{
+  pageNo: number
+  pageSize: number
+  name: string
+  mobile: string
+  industryId: number | undefined
+  level: number | undefined
+  source: number | undefined
+  sceneType: number | undefined
+  pool: boolean | undefined
+}>({
   pageNo: 1,
   pageSize: 10,
-  pool: false,
   name: '',
   mobile: '',
   industryId: undefined,
   level: undefined,
-  source: undefined
+  source: undefined,
+  sceneType: undefined,
+  pool: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
+const activeName = ref('1') // 列表 tab
+
+enum CrmSceneTypeEnum {
+  OWNER = 1,
+  FOLLOW = 2,
+  INVOLVED = 3,
+  SUBORDINATE = 4
+}
+
+const handleClick = (tab: TabsPaneContext) => {
+  switch (tab.paneName) {
+    case '1':
+      resetQuery()
+      break
+    case '2':
+      resetQuery(() => {
+        queryParams.value.sceneType = CrmSceneTypeEnum.OWNER
+      })
+      break
+    case '3':
+      resetQuery(() => {
+        queryParams.value.sceneType = CrmSceneTypeEnum.FOLLOW
+      })
+      break
+    // TODO @puhui999:这个貌似报错?
+    case '4':
+      resetQuery(() => {
+        queryParams.value.sceneType = CrmSceneTypeEnum.INVOLVED
+      })
+      break
+    case '5':
+      resetQuery(() => {
+        queryParams.value.sceneType = CrmSceneTypeEnum.SUBORDINATE
+      })
+      break
+    // TODO @puhui999:公海单独一个菜单哈。
+    case '6':
+      resetQuery(() => {
+        queryParams.value.pool = true
+      })
+      break
+  }
+}
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    const data = await CustomerApi.getCustomerPage(queryParams)
+    const data = await CustomerApi.getCustomerPage(queryParams.value)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -233,19 +305,30 @@ const getList = async () => {
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  queryParams.pageNo = 1
+  queryParams.value.pageNo = 1
   getList()
 }
 
 /** 重置按钮操作 */
-const resetQuery = () => {
+const resetQuery = (func: Function | undefined = undefined) => {
   queryFormRef.value.resetFields()
-  queryParams.pool = false
+  queryParams.value = {
+    pageNo: 1,
+    pageSize: 10,
+    name: '',
+    mobile: '',
+    industryId: undefined,
+    level: undefined,
+    source: undefined,
+    sceneType: undefined,
+    pool: undefined
+  }
+  func && func()
   handleQuery()
 }
 
 /** 打开客户详情 */
-const { push } = useRouter()
+const { currentRoute, push } = useRouter()
 const openDetail = (id: number) => {
   push({ name: 'CrmCustomerDetail', params: { id } })
 }
@@ -276,7 +359,7 @@ const handleExport = async () => {
     await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
-    const data = await CustomerApi.exportCustomer(queryParams)
+    const data = await CustomerApi.exportCustomer(queryParams.value)
     download.excel(data, '客户.xls')
   } catch {
   } finally {
@@ -284,6 +367,14 @@ const handleExport = async () => {
   }
 }
 
+/** 监听路由变化更新列表 */
+watch(
+  () => currentRoute.value,
+  () => {
+    getList()
+  }
+)
+
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 9 - 5
src/views/crm/permission/components/PermissionList.vue

@@ -2,7 +2,8 @@
   <!-- 操作栏 -->
   <el-row justify="end">
     <el-button @click="openForm">
-      <Icon class="mr-5px" icon="fluent:people-team-add-20-filled" /> 添加团队成员
+      <Icon class="mr-5px" icon="fluent:people-team-add-20-filled" />
+      添加团队成员
     </el-button>
     <el-button @click="handleUpdate">
       <Icon class="mr-5px" icon="ep:edit" />
@@ -105,14 +106,14 @@ const handleDelete = async () => {
     message.warning('请先选择团队成员后操作!')
     return
   }
-  // TODO @puhui999:应该有个提示哈
-  await message.delConfirm()
+  await message.delConfirm('是否删除选择的团队成员?')
   const ids = multipleSelection.value?.map((item) => item.id)
   await PermissionApi.deletePermissionBatch({
     bizType: props.bizType,
     bizId: props.bizId,
     ids
   })
+  message.success('删除成功')
 }
 
 /** 退出团队 */
@@ -125,7 +126,7 @@ const handleQuit = async () => {
     message.warning('负责人不能退出团队!')
     return
   }
-  // TODO @puhui999:应该有个提示哈
+  await message.confirm('确认退出团队吗?')
   const userPermission = list.value.find((item) => item.userId === userStore.getUser.id)
   await PermissionApi.deleteSelfPermission(userPermission?.id)
 }
@@ -133,7 +134,10 @@ const handleQuit = async () => {
 /** 监听打开的 bizId + bizType,从而加载最新的列表 */
 watch(
   () => [props.bizId, props.bizType],
-  () => {
+  (val) => {
+    if (!val[0]) {
+      return
+    }
     getList()
   },
   { immediate: true, deep: true }

+ 70 - 64
src/views/mall/product/spu/index.vue

@@ -1,3 +1,4 @@
+<!-- 商品中心 - 商品列表  -->
 <template>
   <!-- 搜索工作栏 -->
   <ContentWrap>
@@ -125,27 +126,33 @@
           </el-form>
         </template>
       </el-table-column>
-      <el-table-column align="center" label="商品编号" min-width="60" prop="id" />
-      <el-table-column label="商品图" min-width="80">
+      <el-table-column label="商品编号" min-width="140" prop="id" />
+      <el-table-column label="商品信息" min-width="300">
         <template #default="{ row }">
-          <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+          <div class="flex">
+            <el-image
+              fit="cover"
+              :src="row.picUrl"
+              class="flex-none w-50px h-50px"
+              @click="imagePreview(row.picUrl)"
+            />
+            <div class="ml-4 overflow-hidden">
+              <el-tooltip effect="dark" :content="row.name" placement="top">
+                <div>
+                  {{ row.name }}
+                </div>
+              </el-tooltip>
+            </div>
+          </div>
         </template>
       </el-table-column>
-      <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
-      <el-table-column align="center" label="商品售价" min-width="90" prop="price">
-        <template #default="{ row }"> {{ fenToYuan(row.price) }}元</template>
+      <el-table-column align="center" label="价格" min-width="160" prop="price">
+        <template #default="{ row }"> ¥ {{ fenToYuan(row.price) }}</template>
       </el-table-column>
       <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
       <el-table-column align="center" label="库存" min-width="90" prop="stock" />
       <el-table-column align="center" label="排序" min-width="70" prop="sort" />
-      <el-table-column
-        :formatter="dateFormatter"
-        align="center"
-        label="创建时间"
-        prop="createTime"
-        width="180"
-      />
-      <el-table-column align="center" label="状态" min-width="80">
+      <el-table-column align="center" label="销售状态" min-width="80">
         <template #default="{ row }">
           <template v-if="row.status >= 0">
             <el-switch
@@ -163,16 +170,16 @@
           </template>
         </template>
       </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
+        width="180"
+      />
       <el-table-column align="center" fixed="right" label="操作" min-width="200">
         <template #default="{ row }">
-          <el-button
-            v-hasPermi="['product:spu:update']"
-            link
-            type="primary"
-            @click="openDetail(row.id)"
-          >
-            详情
-          </el-button>
+          <el-button link type="primary" @click="openDetail(row.id)"> 详情 </el-button>
           <el-button
             v-hasPermi="['product:spu:update']"
             link
@@ -196,17 +203,17 @@
               type="primary"
               @click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)"
             >
-              恢复到仓库
+              恢复
             </el-button>
           </template>
           <template v-else>
             <el-button
               v-hasPermi="['product:spu:update']"
               link
-              type="primary"
+              type="danger"
               @click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)"
             >
-              加入回收
+              回收
             </el-button>
           </template>
         </template>
@@ -236,48 +243,41 @@ defineOptions({ name: 'ProductSpu' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
-const { currentRoute, push } = useRouter() // 路由跳转
+const { push } = useRouter() // 路由跳转
 
 const loading = ref(false) // 列表的加载中
 const exportLoading = ref(false) // 导出的加载中
 const total = ref(0) // 列表的总页数
-const list = ref<any[]>([]) // 列表的数据
+const list = ref<ProductSpuApi.Spu[]>([]) // 列表的数据
 // tabs 数据
 const tabsData = ref([
   {
-    count: 0,
-    name: '出售中商品',
-    type: 0
+    name: '出售中',
+    type: 0,
+    count: 0
   },
   {
-    count: 0,
-    name: '仓库中商品',
-    type: 1
+    name: '仓库中',
+    type: 1,
+    count: 0
   },
   {
-    count: 0,
-    name: '已售罄商品',
-    type: 2
+    name: '已售罄',
+    type: 2,
+    count: 0
   },
   {
-    count: 0,
     name: '警戒库存',
-    type: 3
+    type: 3,
+    count: 0
   },
   {
-    count: 0,
-    name: '商品回收站',
-    type: 4
+    name: '回收站',
+    type: 4,
+    count: 0
   }
 ])
 
-/** 获得每个 Tab 的数量 */
-const getTabsCount = async () => {
-  const res = await ProductSpuApi.getTabsCount()
-  for (let objName in res) {
-    tabsData.value[Number(objName)].count = res[objName]
-  }
-}
 const queryParams = ref({
   pageNo: 1,
   pageSize: 10,
@@ -288,11 +288,6 @@ const queryParams = ref({
 }) // 查询参数
 const queryFormRef = ref() // 搜索的表单Ref
 
-const handleTabClick = (tab: TabsPaneContext) => {
-  queryParams.value.tabType = tab.paneName as number
-  getList()
-}
-
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -305,8 +300,22 @@ const getList = async () => {
   }
 }
 
+/** 切换 Tab */
+const handleTabClick = (tab: TabsPaneContext) => {
+  queryParams.value.tabType = tab.paneName as number
+  getList()
+}
+
+/** 获得每个 Tab 的数量 */
+const getTabsCount = async () => {
+  const res = await ProductSpuApi.getTabsCount()
+  for (let objName in res) {
+    tabsData.value[Number(objName)].count = res[objName]
+  }
+}
+
 /** 添加到仓库 / 回收站的状态 */
-const handleStatus02Change = async (row, newStatus: number) => {
+const handleStatus02Change = async (row: any, newStatus: number) => {
   try {
     // 二次确认
     const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
@@ -322,7 +331,7 @@ const handleStatus02Change = async (row, newStatus: number) => {
 }
 
 /** 更新上架/下架状态 */
-const handleStatusChange = async (row) => {
+const handleStatusChange = async (row: any) => {
   try {
     // 二次确认
     const text = row.status ? '上架' : '下架'
@@ -407,19 +416,16 @@ const handleExport = async () => {
   }
 }
 
-const categoryList = ref() // 分类树
 /** 获取分类的节点的完整结构 */
-const formatCategoryName = (categoryId) => {
+const categoryList = ref() // 分类树
+const formatCategoryName = (categoryId: number) => {
   return treeToString(categoryList.value, categoryId)
 }
 
-// 监听路由变化更新列表,解决商品保存后,列表不刷新的问题。
-watch(
-  () => currentRoute.value,
-  () => {
-    getList()
-  }
-)
+/** 激活时 */
+onActivated(() => {
+  getList()
+})
 
 /** 初始化 **/
 onMounted(async () => {

+ 172 - 43
src/views/mall/promotion/rewardActivity/RewardForm.vue

@@ -24,22 +24,96 @@
           <el-radio
             v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_CONDITION_TYPE)"
             :key="dict.value"
-            :label="parseInt(dict.value)"
-            >{{ dict.label }}</el-radio
+            :label="dict.value"
           >
+            {{ dict.label }}
+          </el-radio>
         </el-radio-group>
       </el-form-item>
       <el-form-item label="优惠设置">
-        <!-- TODO 待实现!这个实现下哈 -->
+        <template v-for="(item, index) in formData.rules" :key="index">
+          <el-row type="flex">
+            <el-col :span="24" style="font-weight: bold; display: flex">
+              活动层级{{ index + 1 }}
+              <el-button
+                link
+                type="danger"
+                style="margin-left: auto"
+                v-if="index != 0"
+                @click="deleteActivityRule(index)"
+              >
+                删除
+              </el-button>
+            </el-col>
+            <e-form :ref="'formRef' + index" :model="item">
+              <el-form-item
+                label="优惠门槛:"
+                prop="limit"
+                label-width="100px"
+                style="padding-left: 50px"
+              >
+                满
+                <el-input
+                  style="width: 150px; padding: 0 10px"
+                  v-model="item.limit"
+                  type="number"
+                  placeholder=""
+                />
+                元
+              </el-form-item>
+              <el-form-item label="优惠内容:" label-width="100px" style="padding-left: 50px">
+                <el-checkbox-group v-model="activityRules[index]" style="width: 100%">
+                  <el-col :span="24">
+                    <el-checkbox label="订单金额优惠" name="type" />
+                    <el-form-item v-if="activityRules[index].includes('订单金额优惠')">
+                      减
+                      <el-input
+                        style="width: 150px; padding: 0 20px"
+                        v-model="item.discountPrice"
+                        type="number"
+                        placeholder=""
+                      />
+                      元
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="24">
+                    <el-checkbox v-model="item.freeDelivery" label="包邮" name="type" />
+                  </el-col>
+                  <el-col :span="24">
+                    <el-checkbox label="送积分" name="type" />
+                    <el-form-item v-if="activityRules[index].includes('送积分')">
+                      送
+                      <el-input
+                        style="width: 150px; padding: 0 20px"
+                        v-model="item.point"
+                        type="number"
+                        placeholder=""
+                      />
+                      积分
+                    </el-form-item>
+                  </el-col>
+                  <!-- 优惠券待处理  也可以参考优惠劵的SpuShowcase-->
+                  <!-- TODO 待实现!-->
+                  <el-col :span="24">
+                    <el-checkbox label="送优惠券" name="type" />
+                  </el-col>
+                </el-checkbox-group>
+              </el-form-item>
+            </e-form>
+          </el-row>
+        </template>
+        <!-- TODO 实现:建议改成放在每一个【活动层级】的下面,有点类似主子表 -->
+        <el-button type="primary" @click="addActivityStratum">添加活动层级</el-button>
       </el-form-item>
       <el-form-item label="活动商品" prop="productScope">
         <el-radio-group v-model="formData.productScope">
           <el-radio
             v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)"
             :key="dict.value"
-            :label="parseInt(dict.value)"
-            >{{ dict.label }}</el-radio
+            :label="dict.value"
           >
+            {{ dict.label }}
+          </el-radio>
         </el-radio-group>
       </el-form-item>
       <!-- TODO:活动商品的开发,可以参考优惠劵的,已经搞好啦; -->
@@ -58,9 +132,9 @@
         >
           <el-option v-for="item in productSpus" :key="item.id" :label="item.name" :value="item.id">
             <span style="float: left">{{ item.name }}</span>
-            <span style="float: right; font-size: 13px; color: #8492a6"
-              >¥{{ (item.price / 100.0).toFixed(2) }}</span
-            >
+            <span style="float: right; font-size: 13px; color: #8492a6">
+              ¥{{ (item.price / 100.0).toFixed(2) }}
+            </span>
           </el-option>
         </el-select>
       </el-form-item>
@@ -77,15 +151,8 @@
 <script lang="ts" setup>
 import { getSpuSimpleList } from '@/api/mall/product/spu'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { CommonStatusEnum } from '@/utils/constants'
-import * as ProductBrandApi from '@/api/mall/product/brand'
-import {
-  PromotionConditionTypeEnum,
-  PromotionProductScopeEnum,
-  PromotionActivityStatusEnum
-} from '@/utils/constants'
-// 商品数据
-const productSpus = ref<any[]>([])
+import * as RewardActivityApi from '@/api/mall/promotion/reward/rewardActivity'
+import { PromotionConditionTypeEnum, PromotionProductScopeEnum } from '@/utils/constants'
 
 /** 初始化 **/
 onMounted(() => {
@@ -98,6 +165,7 @@ defineOptions({ name: 'ProductBrandForm' })
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
+const productSpus = ref<any[]>([]) // 商品数据
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
@@ -112,8 +180,18 @@ const formData = ref({
   remark: undefined,
   productScope: PromotionProductScopeEnum.ALL.scope,
   productSpuIds: undefined,
-  rules: undefined
+  rules: [
+    {
+      limit: undefined,
+      discountPrice: undefined,
+      freeDelivery: undefined,
+      point: undefined,
+      couponIds: [],
+      couponCounts: []
+    }
+  ]
 })
+const activityRules = reactive([]) // 优惠设置。每个元素都是一个 [],放“包邮”、“送积分”、“订单金额优惠”
 const formRules = reactive({
   name: [{ required: true, message: '活动名称不能为空', trigger: 'blur' }],
   startAndEndTime: [{ required: true, message: '活动时间不能为空', trigger: 'blur' }],
@@ -121,7 +199,7 @@ const formRules = reactive({
   productScope: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }],
   productSpuIds: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }]
 })
-const formRef = ref() // 表单 Ref
+const formRef = ref([]) // 表单 Ref
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -133,19 +211,24 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      // formData.value = await ProductBrandApi.getBrand(id)
-      formData.value = {
-        conditionType: 10,
-        description: '',
-        id: undefined,
-        name: '测试活动',
-        picUrl: '',
-        productScope: 2,
-        productSpuIds: [634],
-        remark: '测试备注',
-        startAndEndTime: [new Date(), new Date('2023-12-31')],
-        status: 0
-      }
+      let data = await RewardActivityApi.getReward(id)
+      data.startAndEndTime = [new Date(data.startTime), new Date(data.endTime)]
+      activityRules.splice(0, activityRules.length)
+      data.rules.forEach((item) => {
+        // TODO 是不是不用 reactive,直接 [] 就可以了?
+        let array: string[] = reactive([])
+        if (item.freeDelivery) {
+          array.push('包邮')
+        }
+        if (item.point) {
+          array.push('送积分')
+        }
+        if (item.discountPrice) {
+          array.push('订单金额优惠')
+        }
+        activityRules.push(array)
+      })
+      formData.value = data
     } finally {
       formLoading.value = false
     }
@@ -160,18 +243,28 @@ const submitForm = async () => {
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
-  console.log(formData.value)
-  message.success('已在控制台打印数据')
-  return
+  // 处理下数据兼容接口
+  formData.value.startTime = +new Date(formData.value.startAndEndTime[0])
+  formData.value.endTime = +new Date(formData.value.startAndEndTime[1])
+  activityRules.forEach((item, index) => {
+    formData.value.rules[index].freeDelivery = !!item.includes('包邮')
+    if (!item.includes('送积分')) {
+      formData.value.rules[index].point = undefined
+    }
+    if (!item.includes('订单金额优惠')) {
+      formData.value.rules[index].discountPrice = undefined
+    }
+  })
+
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as ProductBrandApi.BrandVO
+    const data = formData.value as RewardActivityApi.DiscountActivityVO
     if (formType.value === 'create') {
-      await ProductBrandApi.createBrand(data)
+      await RewardActivityApi.createRewardActivity(data)
       message.success(t('common.createSuccess'))
     } else {
-      await ProductBrandApi.updateBrand(data)
+      await RewardActivityApi.updateRewardActivity(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
@@ -182,15 +275,51 @@ const submitForm = async () => {
   }
 }
 
+const addActivityStratum = () => {
+  formData.value.rules.push({
+    limit: undefined,
+    discountPrice: undefined,
+    freeDelivery: undefined,
+    point: undefined,
+    couponIds: [],
+    couponCounts: []
+  })
+  activityRules.push([])
+}
+
+const deleteActivityRule = (index) => {
+  formData.value.rules.splice(index, 1)
+  activityRules.splice(index, 1)
+}
+
 /** 重置表单 */
 const resetForm = () => {
   formData.value = {
     id: undefined,
-    name: '',
-    picUrl: '',
-    status: CommonStatusEnum.ENABLE,
-    description: ''
+    name: undefined,
+    startAndEndTime: undefined,
+    startTime: undefined,
+    endTime: undefined,
+    conditionType: PromotionConditionTypeEnum.PRICE.type,
+    remark: undefined,
+    productScope: PromotionProductScopeEnum.ALL.scope,
+    productSpuIds: undefined,
+    rules: [
+      {
+        limit: undefined,
+        discountPrice: undefined,
+        freeDelivery: undefined,
+        point: undefined,
+        couponIds: [],
+        couponCounts: []
+      }
+    ]
   }
-  formRef.value?.resetFields()
+  activityRules.splice(0, activityRules.length)
+  activityRules.push(reactive([]))
+  // 解决下有时刷新页面第一次点编辑报错
+  nextTick(() => {
+    formRef.value?.resetFields()
+  })
 }
 </script>

+ 5 - 27
src/views/mall/promotion/rewardActivity/index.vue

@@ -65,13 +65,13 @@
       <el-table-column
         label="活动开始时间"
         align="center"
-        prop="sort[0]"
+        prop="startTime"
         :formatter="dateFormatter"
       />
       <el-table-column
         label="活动结束时间"
         align="center"
-        prop="sort[1]"
+        prop="endTime"
         :formatter="dateFormatter"
       />
       <el-table-column label="状态" align="center" prop="status">
@@ -122,7 +122,7 @@
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import * as ProductBrandApi from '@/api/mall/product/brand'
+import * as RewardActivityApi from '@/api/mall/promotion/reward/rewardActivity'
 import RewardForm from './RewardForm.vue'
 
 defineOptions({ name: 'PromotionRewardActivity' })
@@ -146,22 +146,7 @@ const queryFormRef = ref() // 搜索的表单
 const getList = async () => {
   loading.value = true
   try {
-    // const data = await ProductBrandApi.getBrandParam(queryParams)
-    const data = {
-      list: [
-        {
-          createTime: 1693463998000,
-          description: '',
-          id: 3,
-          name: '索尼',
-          picUrl:
-            'http://127.0.0.1:48080/admin-api/infra/file/4/get/f5b7a536306cd1180a42a2211a8212dc23de6b949d30c30d036caa063042f928.png',
-          sort: [+new Date(), +new Date('2023-12-31')],
-          status: 10
-        }
-      ],
-      total: 1
-    }
+    const data = await RewardActivityApi.getRewardActivityPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -171,16 +156,11 @@ const getList = async () => {
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  console.log(queryParams)
-  message.success('已打印搜索参数')
-  return
   getList()
 }
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  message.success('重置查询表单获取数据')
-  return
   queryFormRef.value.resetFields()
   handleQuery()
 }
@@ -196,10 +176,8 @@ const handleDelete = async (id: number) => {
   try {
     // 删除的二次确认
     await message.delConfirm()
-    message.success('您以确认删除')
-    return
     // 发起删除
-    await ProductBrandApi.deleteBrand(id)
+    await RewardActivityApi.deleteRewardActivity(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()

+ 3 - 6
src/views/mall/statistics/product/components/ProductRank.vue

@@ -77,12 +77,9 @@ const queryParams = reactive({
   times: [],
   sortingFields: {}
 })
-// 列表的加载中
-const loading = ref(false)
-// 列表的总页数
-const total = ref(0)
-// 列表的数据
-const list = ref<ProductStatisticsVO[]>([])
+const loading = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref<ProductStatisticsVO[]>([]) // 列表的数据
 
 /** 查询商品列表 */
 const getSpuList = async () => {

+ 0 - 13
src/views/mall/trade/config/index.vue

@@ -186,17 +186,6 @@
             </el-checkbox-group>
             <el-text class="w-full" size="small" type="info"> 商城开通提现的付款方式 </el-text>
           </el-form-item>
-          <el-form-item label="提现银行" prop="brokerageBankNames">
-            <el-select v-model="formData.brokerageBankNames" placeholder="请选择提现银行" multiple>
-              <el-option
-                v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_BANK_NAME)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-            <el-text class="w-full" size="small" type="info"> 商城开通提现的银行列表 </el-text>
-          </el-form-item>
         </el-tab-pane>
       </el-tabs>
       <!-- 保存 -->
@@ -232,7 +221,6 @@ const formData = ref({
   brokerageSecondPercent: 0,
   brokerageWithdrawMinPrice: 0,
   brokerageWithdrawFeePercent: 0,
-  brokerageBankNames: [],
   brokerageFrozenDays: 0,
   brokerageWithdrawTypes: []
 })
@@ -246,7 +234,6 @@ const formRules = reactive({
     { required: true, message: '用户提现最低金额不能为空', trigger: 'blur' }
   ],
   brokerageWithdrawFeePercent: [{ required: true, message: '提现手续费不能为空', trigger: 'blur' }],
-  brokerageBankNames: [{ required: true, message: '提现银行不能为空', trigger: 'blur' }],
   brokerageFrozenDays: [{ required: true, message: '佣金冻结时间不能为空', trigger: 'blur' }],
   brokerageWithdrawTypes: [
     {