醉梦人间三千年 7 月之前
父节点
当前提交
29e5815e05
共有 50 个文件被更改,包括 6622 次插入0 次删除
  1. 40 0
      src/api/service/category/index.ts
  2. 49 0
      src/api/service/project/index.ts
  3. 179 0
      src/views/coach/user/UserForm.vue
  4. 101 0
      src/views/coach/user/UserLevelUpdateForm.vue
  5. 128 0
      src/views/coach/user/UserPointUpdateForm.vue
  6. 14 0
      src/views/coach/user/components/balance-list.vue
  7. 87 0
      src/views/coach/user/detail/UserAccountInfo.vue
  8. 54 0
      src/views/coach/user/detail/UserAddressList.vue
  9. 85 0
      src/views/coach/user/detail/UserBasicInfo.vue
  10. 125 0
      src/views/coach/user/detail/UserBrokerageList.vue
  11. 190 0
      src/views/coach/user/detail/UserCouponList.vue
  12. 158 0
      src/views/coach/user/detail/UserExperienceRecordList.vue
  13. 96 0
      src/views/coach/user/detail/UserFavoriteList.vue
  14. 279 0
      src/views/coach/user/detail/UserOrderList.vue
  15. 152 0
      src/views/coach/user/detail/UserPointList.vue
  16. 135 0
      src/views/coach/user/detail/UserSignList.vue
  17. 135 0
      src/views/coach/user/detail/index.vue
  18. 295 0
      src/views/coach/user/index.vue
  19. 122 0
      src/views/service/category/UserForm.vue
  20. 101 0
      src/views/service/category/UserLevelUpdateForm.vue
  21. 128 0
      src/views/service/category/UserPointUpdateForm.vue
  22. 14 0
      src/views/service/category/components/balance-list.vue
  23. 87 0
      src/views/service/category/detail/UserAccountInfo.vue
  24. 54 0
      src/views/service/category/detail/UserAddressList.vue
  25. 85 0
      src/views/service/category/detail/UserBasicInfo.vue
  26. 125 0
      src/views/service/category/detail/UserBrokerageList.vue
  27. 190 0
      src/views/service/category/detail/UserCouponList.vue
  28. 158 0
      src/views/service/category/detail/UserExperienceRecordList.vue
  29. 96 0
      src/views/service/category/detail/UserFavoriteList.vue
  30. 279 0
      src/views/service/category/detail/UserOrderList.vue
  31. 152 0
      src/views/service/category/detail/UserPointList.vue
  32. 135 0
      src/views/service/category/detail/UserSignList.vue
  33. 135 0
      src/views/service/category/detail/index.vue
  34. 205 0
      src/views/service/category/index.vue
  35. 231 0
      src/views/service/project/UserForm.vue
  36. 101 0
      src/views/service/project/UserLevelUpdateForm.vue
  37. 128 0
      src/views/service/project/UserPointUpdateForm.vue
  38. 14 0
      src/views/service/project/components/balance-list.vue
  39. 87 0
      src/views/service/project/detail/UserAccountInfo.vue
  40. 54 0
      src/views/service/project/detail/UserAddressList.vue
  41. 85 0
      src/views/service/project/detail/UserBasicInfo.vue
  42. 125 0
      src/views/service/project/detail/UserBrokerageList.vue
  43. 190 0
      src/views/service/project/detail/UserCouponList.vue
  44. 158 0
      src/views/service/project/detail/UserExperienceRecordList.vue
  45. 96 0
      src/views/service/project/detail/UserFavoriteList.vue
  46. 279 0
      src/views/service/project/detail/UserOrderList.vue
  47. 152 0
      src/views/service/project/detail/UserPointList.vue
  48. 135 0
      src/views/service/project/detail/UserSignList.vue
  49. 135 0
      src/views/service/project/detail/index.vue
  50. 284 0
      src/views/service/project/index.vue

+ 40 - 0
src/api/service/category/index.ts

@@ -0,0 +1,40 @@
+import request from '@/config/axios'
+
+export interface Category {
+  id: number
+  title: string
+  cover: string
+  sort: number
+  status: number
+  createTime: Date
+}
+
+// 查询菜单(精简)列表
+export const getSimpleMenusList = () => {
+  return request.get({ url: '/system/menu/simple-list' })
+}
+
+// 查询菜单列表
+export const getCategoryList = (params) => {
+  return request.get({ url: '/service/category', params })
+}
+
+// 获取菜单详情
+export const getCategory = (id: number) => {
+  return request.get({ url: '/service/category/' + id })
+}
+
+// 新增菜单
+export const createCategory = (data: Category) => {
+  return request.post({ url: '/service/category', data })
+}
+
+// 修改菜单
+export const updateCategory = (data: Category) => {
+  return request.put({ url: '/service/category/'+data.id, data })
+}
+
+// 删除菜单
+export const deleteCategory = (id: number) => {
+  return request.delete({ url: '/service/category/' + id })
+}

+ 49 - 0
src/api/service/project/index.ts

@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+export interface Project {
+  id: number,
+  title: string,
+  subTitle: string,
+  status: number,
+  category: number[],
+  cover: string,
+  price: number,
+  initPrice: number,
+  materialPrice: number,
+  sale: number,
+  timeLong: number,
+  comBalance: number,
+  sort: number,
+  isStore: boolean,
+  isDoor: boolean,
+  isAdd: boolean,
+  introduce: string,
+  explain: string,
+  notice: string,
+  createTime: Date
+}
+
+// 查询服务项目列表
+export const getProjectPage = (params) => {
+  return request.get({ url: '/service/project', params })
+}
+
+// 获取服务项目详情
+export const getProject = (id: number) => {
+  return request.get({ url: '/service/project/' + id })
+}
+
+// 新增服务项目
+export const createProject = (data: Project) => {
+  return request.post({ url: '/service/project', data })
+}
+
+// 修改服务项目
+export const updateProject = (data: Project) => {
+  return request.put({ url: '/service/project/'+data.id, data })
+}
+
+// 删除服务项目
+export const deleteProject = (id: number) => {
+  return request.delete({ url: '/service/project/' + id })
+}

+ 179 - 0
src/views/coach/user/UserForm.vue

@@ -0,0 +1,179 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="手机号" prop="mobile">
+        <el-input v-model="formData.mobile" placeholder="请输入手机号" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickname">
+        <el-input v-model="formData.nickname" placeholder="请输入用户昵称" />
+      </el-form-item>
+      <el-form-item label="头像" prop="avatar">
+        <UploadImg v-model="formData.avatar" :limit="1" :is-show-tip="false" />
+      </el-form-item>
+      <el-form-item label="真实名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入真实名字" />
+      </el-form-item>
+      <el-form-item label="用户性别" prop="sex">
+        <el-radio-group v-model="formData.sex">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="所在地" prop="areaId">
+        <el-tree-select
+          v-model="formData.areaId"
+          :data="areaList"
+          :props="defaultProps"
+          :render-after-expand="true"
+        />
+      </el-form-item>
+      <el-form-item label="用户标签" prop="tagIds">
+        <MemberTagSelect v-model="formData.tagIds" show-add />
+      </el-form-item>
+      <el-form-item label="用户分组" prop="groupId">
+        <MemberGroupSelect v-model="formData.groupId" />
+      </el-form-item>
+      <el-form-item label="会员备注" prop="mark">
+        <el-input type="textarea" v-model="formData.mark" placeholder="请输入会员备注" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as UserApi from '@/api/member/user'
+import * as AreaApi from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
+import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  mobile: undefined,
+  password: undefined,
+  status: undefined,
+  nickname: undefined,
+  avatar: undefined,
+  name: undefined,
+  sex: undefined,
+  areaId: undefined,
+  birthday: undefined,
+  mark: undefined,
+  tagIds: [],
+  groupId: undefined
+})
+const formRules = reactive({
+  mobile: [{ required: true, message: '手机号不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const areaList = 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 UserApi.getUser(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 获得地区列表
+  areaList.value = await AreaApi.getAreaTree()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as UserApi.UserVO
+    if (formType.value === 'create') {
+      // 说明:目前暂时没有新增操作。如果自己业务需要,可以进行扩展
+      // await UserApi.createUser(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await UserApi.updateUser(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    mobile: undefined,
+    password: undefined,
+    status: undefined,
+    nickname: undefined,
+    avatar: undefined,
+    name: undefined,
+    sex: undefined,
+    areaId: undefined,
+    birthday: undefined,
+    mark: undefined,
+    tagIds: [],
+    groupId: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 101 - 0
src/views/coach/user/UserLevelUpdateForm.vue

@@ -0,0 +1,101 @@
+<template>
+  <Dialog title="修改用户等级" v-model="dialogVisible" width="600">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="用户编号" prop="id">
+        <el-input v-model="formData.id" placeholder="请输入用户昵称" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickname">
+        <el-input
+          v-model="formData.nickname"
+          placeholder="请输入用户昵称"
+          class="!w-240px"
+          disabled
+        />
+      </el-form-item>
+      <el-form-item label="用户等级" prop="levelId">
+        <MemberLevelSelect v-model="formData.levelId" />
+      </el-form-item>
+      <el-form-item label="修改原因" prop="reason">
+        <el-input type="textarea" v-model="formData.reason" placeholder="请输入修改原因" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  nickname: undefined,
+  levelId: undefined,
+  reason: undefined
+})
+const formRules = reactive({
+  reason: [{ required: true, message: '修改原因不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  dialogVisible.value = true
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await UserApi.getUser(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    await UserApi.updateUserLevel(formData.value)
+
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    nickname: undefined,
+    levelId: undefined,
+    reason: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 128 - 0
src/views/coach/user/UserPointUpdateForm.vue

@@ -0,0 +1,128 @@
+<template>
+  <Dialog title="修改用户积分" v-model="dialogVisible" width="600">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="用户编号" prop="id">
+        <el-input v-model="formData.id" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickname">
+        <el-input v-model="formData.nickname" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="变动前积分" prop="point">
+        <el-input-number v-model="formData.point" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="变动类型" prop="changeType">
+        <el-radio-group v-model="formData.changeType">
+          <el-radio :label="1">增加</el-radio>
+          <el-radio :label="-1">减少</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="变动积分" prop="changePoint">
+        <el-input-number v-model="formData.changePoint" class="!w-240px" :min="0" :precision="0" />
+      </el-form-item>
+      <el-form-item label="变动后积分">
+        <el-input-number v-model="pointResult" class="!w-240px" disabled />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+
+/** 修改用户积分表单 */
+defineOptions({ name: 'UpdatePointForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  nickname: undefined,
+  point: 0,
+  changePoint: 0,
+  changeType: 1
+})
+const formRules = reactive({
+  changePoint: [{ required: true, message: '变动积分不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  dialogVisible.value = true
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await UserApi.getUser(id)
+      formData.value.changeType = 1 // 默认增加积分
+      formData.value.changePoint = 0 // 变动积分默认0
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+
+  if (formData.value.changePoint < 1) {
+    message.error('变动积分不能小于 1')
+    return
+  }
+  if (pointResult.value < 0) {
+    message.error('变动后的积分不能小于 0')
+    return
+  }
+
+  // 提交请求
+  formLoading.value = true
+  try {
+    await UserApi.updateUserPoint({
+      id: formData.value.id,
+      point: formData.value.changePoint * formData.value.changeType
+    })
+
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    nickname: undefined,
+    levelId: undefined,
+    reason: undefined
+  }
+  formRef.value?.resetFields()
+}
+
+/** 变动后的积分 */
+const pointResult = computed(
+  () => formData.value.point + formData.value.changePoint * formData.value.changeType
+)
+</script>

+ 14 - 0
src/views/coach/user/components/balance-list.vue

@@ -0,0 +1,14 @@
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  name: 'BalanceList'
+})
+</script>
+
+<!-- TODO @芋艿:未来实现,等周建的 -->
+<template>
+  <div>余额列表</div>
+</template>
+
+<style scoped lang="scss"></style>

+ 87 - 0
src/views/coach/user/detail/UserAccountInfo.vue

@@ -0,0 +1,87 @@
+<template>
+  <el-descriptions :column="2">
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 等级 " icon="svg-icon:member_level" />
+      </template>
+      {{ user.levelName || '无' }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 成长值 " icon="ep:suitcase" />
+      </template>
+      {{ user.experience || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 当前积分 " icon="ep:coin" />
+      </template>
+      {{ user.point || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 总积分 " icon="ep:coin" />
+      </template>
+      {{ user.totalPoint || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 当前余额 " icon="svg-icon:member_balance" />
+      </template>
+      {{ fenToYuan(wallet.balance || 0) }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 支出金额 " icon="svg-icon:member_expenditure_balance" />
+      </template>
+      {{ fenToYuan(wallet.totalExpense || 0) }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 充值金额 " icon="svg-icon:member_recharge_balance" />
+      </template>
+      {{ fenToYuan(wallet.totalRecharge || 0) }}
+    </el-descriptions-item>
+  </el-descriptions>
+</template>
+<script setup lang="ts">
+import { DescriptionsItemLabel } from '@/components/Descriptions'
+import * as UserApi from '@/api/member/user'
+import * as WalletApi from '@/api/pay/wallet/balance'
+import { UserTypeEnum } from '@/utils/constants'
+import { fenToYuan } from '@/utils'
+
+const props = defineProps<{ user: UserApi.UserVO }>() // 用户信息
+const WALLET_INIT_DATA = {
+  balance: 0,
+  totalExpense: 0,
+  totalRecharge: 0
+} as WalletApi.WalletVO // 钱包初始化数据
+const wallet = ref<WalletApi.WalletVO>(WALLET_INIT_DATA) // 钱包信息
+
+/** 查询用户钱包信息 */
+const getUserWallet = async () => {
+  if (!props.user.id) {
+    wallet.value = WALLET_INIT_DATA
+    return
+  }
+  const params = { userId: props.user.id }
+  wallet.value = (await WalletApi.getWallet(params)) || WALLET_INIT_DATA
+}
+
+/** 监听用户编号变化 */
+watch(
+  () => props.user.id,
+  () => getUserWallet(),
+  { immediate: true }
+)
+</script>
+<style scoped lang="scss">
+.cell-item {
+  display: inline;
+}
+
+.cell-item::after {
+  content: ':';
+}
+</style>

+ 54 - 0
src/views/coach/user/detail/UserAddressList.vue

@@ -0,0 +1,54 @@
+<template>
+  <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+    <el-table-column label="地址编号" align="center" prop="id" width="150px" />
+    <el-table-column label="收件人名称" align="center" prop="name" width="150px" />
+    <el-table-column label="手机号" align="center" prop="mobile" width="150px" />
+    <el-table-column label="地区编码" align="center" prop="areaId" width="150px" />
+    <el-table-column label="收件详细地址" align="center" prop="detailAddress" />
+    <el-table-column label="是否默认" align="center" prop="defaultStatus" width="150px">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="Number(scope.row.defaultStatus)" />
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="创建时间"
+      align="center"
+      prop="createTime"
+      :formatter="dateFormatter"
+      width="180px"
+    />
+  </el-table>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as AddressApi from '@/api/member/address'
+
+const { userId }: { userId: number } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await AddressApi.getAddressList({ userId })
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 85 - 0
src/views/coach/user/detail/UserBasicInfo.vue

@@ -0,0 +1,85 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <slot name="header"></slot>
+    </template>
+    <el-row >
+      <el-col :span="5">
+        <ElAvatar shape="square" :size="140" :src="user.avatar || undefined" />
+      </el-col>
+      <el-col :span="19">
+        <el-descriptions :column="2">
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="用户名" icon="ep:user" />
+            </template>
+            {{ user.name || '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="昵称" icon="ep:user" />
+            </template>
+            {{ user.nickname }}
+          </el-descriptions-item>
+          <el-descriptions-item label="手机号">
+            <template #label>
+              <descriptions-item-label label="手机号" icon="ep:phone" />
+            </template>
+            {{ user.mobile }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="性别" icon="fa:mars-double" />
+            </template>
+            <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="user.sex" />
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="所在地" icon="ep:location" />
+            </template>
+            {{ user.areaName }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="注册 IP" icon="ep:position" />
+            </template>
+            {{ user.registerIp }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="生日" icon="fa:birthday-cake" />
+            </template>
+            {{ user.birthday ? formatDate(user.birthday) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="注册时间" icon="ep:calendar" />
+            </template>
+            {{ user.createTime ? formatDate(user.createTime) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="最后登录时间" icon="ep:calendar" />
+            </template>
+            {{ user.loginDate ? formatDate(user.loginDate) : '空' }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-col>
+    </el-row>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as UserApi from '@/api/member/user'
+import { DescriptionsItemLabel } from '@/components/Descriptions/index'
+
+const { user } = defineProps<{ user: UserApi.UserVO }>()
+</script>
+<style scoped lang="scss">
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 125 - 0
src/views/coach/user/detail/UserBrokerageList.vue

@@ -0,0 +1,125 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="85px"
+    >
+      <el-form-item label="用户类型" prop="level">
+        <el-radio-group v-model="queryParams.level" @change="handleQuery">
+          <el-radio-button checked>全部</el-radio-button>
+          <el-radio-button label="1">一级推广人</el-radio-button>
+          <el-radio-button label="2">二级推广人</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="绑定时间" prop="bindUserTime">
+        <el-date-picker
+          v-model="queryParams.bindUserTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="用户编号" align="center" prop="id" min-width="80px" />
+      <el-table-column label="头像" align="center" prop="avatar" width="70px">
+        <template #default="scope">
+          <el-avatar :src="scope.row.avatar" />
+        </template>
+      </el-table-column>
+      <el-table-column label="昵称" align="center" prop="nickname" min-width="80px" />
+      <el-table-column label="等级" align="center" prop="level" min-width="80px">
+        <template #default="scope">
+          <el-tag v-if="scope.row.bindUserId === bindUserId">一级</el-tag>
+          <el-tag v-else>二级</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="绑定时间"
+        align="center"
+        prop="bindUserTime"
+        :formatter="dateFormatter"
+        width="170px"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+
+/** 推广人列表 */
+defineOptions({ name: 'UserBrokerageList' })
+
+const { bindUserId }: { bindUserId: number } = defineProps({
+  bindUserId: {
+    type: Number,
+    required: true
+  }
+}) //用户编号
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  bindUserId: null,
+  level: '',
+  bindUserTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.bindUserId = bindUserId
+    const data = await BrokerageUserApi.getBrokerageUserPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 190 - 0
src/views/coach/user/detail/UserCouponList.vue

@@ -0,0 +1,190 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />搜索 </el-button>
+        <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <!-- Tab 选项:真正的内容在 Lab -->
+    <el-tabs v-model="activeTab" type="card" @tab-change="onTabChange">
+      <el-tab-pane
+        v-for="tab in statusTabs"
+        :key="tab.value"
+        :label="tab.label"
+        :name="tab.value"
+      />
+    </el-tabs>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="优惠劵" align="center" prop="name" />
+      <el-table-column label="优惠券类型" align="center" prop="discountType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="领取方式" align="center" prop="takeType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="领取时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column
+        label="使用时间"
+        align="center"
+        prop="useTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['promotion:coupon:delete']"
+            type="danger"
+            link
+            @click="handleDelete(scope.row.id)"
+          >
+            回收
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts" name="UserCouponList">
+import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon/coupon'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'UserCouponList' })
+
+const { userId }: { userId: number } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+}) //用户编号
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 字典表格数据
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  createTime: [],
+  status: undefined,
+  userIds: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+const activeTab = ref('all') // Tab 筛选
+const statusTabs = reactive([
+  {
+    label: '全部',
+    value: 'all'
+  }
+])
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  // 执行查询
+  try {
+    queryParams.userIds = userId
+    const data = await getCouponPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 二次确认
+    await message.confirm(
+      '回收将会收回会员领取的待使用的优惠券,已使用的将无法回收,确定要回收所选优惠券吗?'
+    )
+    // 发起删除
+    await deleteCoupon(id)
+    message.notifySuccess('回收成功')
+    // 重新加载列表
+    await getList()
+  } catch {}
+}
+
+/** tab 切换 */
+const onTabChange = (tabName) => {
+  queryParams.status = tabName === 'all' ? undefined : tabName
+  getList()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+  // 设置 statuses 过滤
+  for (const dict of getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_STATUS)) {
+    statusTabs.push({
+      label: dict.label,
+      value: dict.value as string
+    })
+  }
+})
+</script>

+ 158 - 0
src/views/coach/user/detail/UserExperienceRecordList.vue

@@ -0,0 +1,158 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="业务类型" prop="bizType">
+        <el-select
+          v-model="queryParams.bizType"
+          placeholder="请选择业务类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="标题" prop="title">
+        <el-input
+          v-model="queryParams.title"
+          placeholder="请输入标题"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" width="150px" />
+      <el-table-column
+        label="获得时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="经验" align="center" prop="experience" width="150px">
+        <template #default="scope">
+          <el-tag v-if="scope.row.experience > 0" class="ml-2" type="success" effect="dark">
+            +{{ scope.row.experience }}
+          </el-tag>
+          <el-tag v-else class="ml-2" type="danger" effect="dark">
+            {{ scope.row.experience }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="总经验" align="center" prop="totalExperience" width="150px">
+        <template #default="scope">
+          <el-tag class="ml-2" effect="dark">
+            {{ scope.row.totalExperience }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="标题" align="center" prop="title" width="150px" />
+      <el-table-column label="描述" align="center" prop="description" />
+      <el-table-column label="业务编号" align="center" prop="bizId" width="150px" />
+      <el-table-column label="业务类型" align="center" prop="bizType" width="150px">
+        <!--   TODO 芋艿:此处应创建对应的字典 -->
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE" :value="scope.row.bizType" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as ExperienceRecordApi from '@/api/member/experience-record/index'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'UserExperienceRecordList' })
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: null,
+  bizId: null,
+  bizType: null,
+  title: null,
+  description: null,
+  experience: null,
+  totalExperience: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ExperienceRecordApi.getExperienceRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 96 - 0
src/views/coach/user/detail/UserFavoriteList.vue

@@ -0,0 +1,96 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column key="id" align="center" label="商品编号" width="180" prop="id" />
+      <el-table-column label="商品图" min-width="80">
+        <template #default="{ row }">
+          <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+        </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 }"> {{ floatToFixed2(row.price) }}元</template>
+      </el-table-column>
+      <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="收藏时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="状态" min-width="80">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PRODUCT_SPU_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as FavoriteApi from '@/api/mall/product/favorite'
+import { floatToFixed2 } from '@/utils'
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  createTime: [],
+  userId: NaN
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await FavoriteApi.getFavoritePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 279 - 0
src/views/coach/user/detail/UserOrderList.vue

@@ -0,0 +1,279 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="订单状态" prop="status">
+        <el-select v-model="queryParams.status" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="支付方式" prop="payChannelCode">
+        <el-select
+          v-model="queryParams.payChannelCode"
+          class="!w-280px"
+          clearable
+          placeholder="全部"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-280px"
+          end-placeholder="自定义时间"
+          start-placeholder="自定义时间"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="订单来源" prop="terminal">
+        <el-select v-model="queryParams.terminal" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TERMINAL)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="订单类型" prop="type">
+        <el-select v-model="queryParams.type" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="配送方式" prop="deliveryType">
+        <el-select v-model="queryParams.deliveryType" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+        label="快递公司"
+        prop="logisticsId"
+      >
+        <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="item in deliveryExpressList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+        label="自提门店"
+        prop="pickUpStoreId"
+      >
+        <el-select
+          v-model="queryParams.pickUpStoreId"
+          class="!w-280px"
+          clearable
+          multiple
+          placeholder="全部"
+        >
+          <el-option
+            v-for="item in pickUpStoreList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+        label="核销码"
+        prop="pickUpVerifyCode"
+      >
+        <el-input
+          v-model="queryParams.pickUpVerifyCode"
+          class="!w-280px"
+          clearable
+          placeholder="请输入自提核销码"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="聚合搜索">
+        <el-input
+          v-show="true"
+          v-model="queryParams[queryType.queryParam]"
+          class="!w-280px"
+          clearable
+          placeholder="请输入"
+        >
+          <template #prepend>
+            <el-select
+              v-model="queryType.queryParam"
+              class="!w-110px"
+              clearable
+              placeholder="全部"
+              @change="inputChangeSelect"
+            >
+              <el-option
+                v-for="dict in dynamicSearchList"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-input>
+      </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-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <!-- 添加 row-key="id" 解决列数据中的 table#header 数据不刷新的问题  -->
+    <el-table v-loading="loading" :data="list" row-key="id">
+<!--      <OrderTableColumn :list="list" :pick-up-store-list="pickUpStoreList">-->
+<!--        <template #default="{ row }">-->
+<!--          <el-button link type="primary" @click="openDetail(row.id)">-->
+<!--            <Icon icon="ep:notification" />-->
+<!--            详情-->
+<!--          </el-button>-->
+<!--        </template>-->
+<!--      </OrderTableColumn>-->
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as OrderApi from '@/api/mall/trade/order/index'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import { FormInstance } from 'element-plus'
+// import { OrderTableColumn } from '@/views/mall/trade/order/components'
+import { DeliveryTypeEnum } from '@/utils/constants'
+
+const { push } = useRouter() // 路由跳转
+
+const { userId } = defineProps<{
+  userId: number
+}>()
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const pickUpStoreList = ref<PickUpStoreApi.DeliveryPickUpStoreVO[]>([]) // 自提门店精简列表
+const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 物流公司
+const queryFormRef = ref<FormInstance>() // 搜索的表单
+// 表单搜索
+const queryParams = ref({
+  pageNo: 1, // 页数
+  pageSize: 10, // 每页显示数量
+  userId: userId,
+  status: undefined, // 订单状态
+  payChannelCode: undefined, // 支付方式
+  createTime: undefined, // 创建时间
+  terminal: undefined, // 订单来源
+  type: undefined, // 订单类型
+  deliveryType: undefined, // 配送方式
+  logisticsId: undefined, // 快递公司
+  pickUpStoreId: undefined, // 自提门店
+  pickUpVerifyCode: undefined // 自提核销码
+})
+const queryType = reactive({ queryParam: '' }) // 订单搜索类型 queryParam
+
+// 订单聚合搜索 select 类型配置(动态搜索)
+const dynamicSearchList = ref([
+  { value: 'no', label: '订单号' },
+  { value: 'userNickname', label: '用户昵称' },
+  { value: 'userMobile', label: '用户电话' }
+])
+/**
+ * 聚合搜索切换查询对象时触发
+ * @param val
+ */
+const inputChangeSelect = (val: string) => {
+  dynamicSearchList.value
+    .filter((item) => item.value !== val)
+    ?.forEach((item1) => {
+      // 清除集合搜索无用属性
+      if (queryParams.value.hasOwnProperty(item1.value)) {
+        delete queryParams.value[item1.value]
+      }
+    })
+}
+
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  queryParams.value.pageNo = 1
+  await getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.value.userId = userId
+  handleQuery()
+}
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await OrderApi.getOrderPage(queryParams.value)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 查看订单详情 */
+const openDetail = (id: number) => {
+  push({ name: 'TradeOrderDetail', params: { id } })
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+  deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+})
+</script>

+ 152 - 0
src/views/coach/user/detail/UserPointList.vue

@@ -0,0 +1,152 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="业务类型" prop="bizType">
+        <el-select
+          v-model="queryParams.bizType"
+          placeholder="请选择业务类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_POINT_BIZ_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="积分标题" prop="title">
+        <el-input
+          v-model="queryParams.title"
+          placeholder="请输入积分标题"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="获得时间" prop="createDate">
+        <el-date-picker
+          v-model="queryParams.createDate"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" width="180" />
+      <el-table-column
+        label="获得时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column label="获得积分" align="center" prop="point" width="100">
+        <template #default="scope">
+          <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+            +{{ scope.row.point }}
+          </el-tag>
+          <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="总积分" align="center" prop="totalPoint" width="100" />
+      <el-table-column label="标题" align="center" prop="title" />
+      <el-table-column label="描述" align="center" prop="description" />
+      <el-table-column label="业务编码" align="center" prop="bizId" />
+      <el-table-column label="业务类型" align="center" prop="bizType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.MEMBER_POINT_BIZ_TYPE" :value="scope.row.bizType" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as RecordApi from '@/api//member/point/record'
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  bizType: undefined,
+  title: null,
+  createDate: [],
+  userId: NaN
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await RecordApi.getRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 135 - 0
src/views/coach/user/detail/UserSignList.vue

@@ -0,0 +1,135 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="签到用户" prop="nickname">
+        <el-input
+          v-model="queryParams.nickname"
+          placeholder="请输入签到用户"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="签到天数" prop="day">
+        <el-input
+          v-model="queryParams.day"
+          placeholder="请输入签到天数"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="签到时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column
+        label="签到天数"
+        align="center"
+        prop="day"
+        :formatter="(_, __, cellValue) => ['第', cellValue, '天'].join(' ')"
+      />
+      <el-table-column label="获得积分" align="center" prop="point" width="100">
+        <template #default="scope">
+          <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+            +{{ scope.row.point }}
+          </el-tag>
+          <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="签到时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as SignInRecordApi from '@/api/member/signin/record'
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: NaN,
+  nickname: null,
+  day: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SignInRecordApi.getSignInRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 135 - 0
src/views/coach/user/detail/index.vue

@@ -0,0 +1,135 @@
+<template>
+  <div v-loading="loading">
+    <el-row :gutter="10">
+      <!-- 左上角:基本信息 -->
+      <el-col :span="14" class="detail-info-item">
+        <UserBasicInfo :user="user">
+          <template #header>
+            <div class="card-header">
+              <CardTitle title="基本信息" />
+              <el-button type="primary" size="small" text @click="openForm('update')">
+                编辑
+              </el-button>
+            </div>
+          </template>
+        </UserBasicInfo>
+      </el-col>
+      <!-- 右上角:账户信息 -->
+      <el-col :span="10" class="detail-info-item">
+        <el-card shadow="never" class="h-full">
+          <template #header>
+            <CardTitle title="账户信息" />
+          </template>
+          <UserAccountInfo :user="user" />
+        </el-card>
+      </el-col>
+      <!-- 下边:账户明细 -->
+      <!-- TODO 芋艿:【订单管理】【售后管理】【收藏记录】-->
+      <el-card header="账户明细" style="width: 100%; margin-top: 20px" shadow="never">
+        <template #header>
+          <CardTitle title="账户明细" />
+        </template>
+        <el-tabs>
+          <el-tab-pane label="积分">
+            <UserPointList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="签到" lazy>
+            <UserSignList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="成长值" lazy>
+            <UserExperienceRecordList :user-id="id" />
+          </el-tab-pane>
+          <!-- TODO @jason:增加一个余额变化; -->
+          <el-tab-pane label="余额" lazy>余额(WIP)</el-tab-pane>
+          <el-tab-pane label="收货地址" lazy>
+            <UserAddressList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="订单管理" lazy>
+            <UserOrderList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="售后管理" lazy>售后管理(WIP)</el-tab-pane>
+          <el-tab-pane label="收藏记录" lazy>
+            <UserFavoriteList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="优惠劵" lazy>
+            <UserCouponList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="推广用户" lazy>
+            <UserBrokerageList :bind-user-id="id" />
+          </el-tab-pane>
+        </el-tabs>
+      </el-card>
+    </el-row>
+  </div>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <UserForm ref="formRef" @success="getUserData(id)" />
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import UserForm from '@/views/member/user/UserForm.vue'
+import UserAccountInfo from './UserAccountInfo.vue'
+import UserAddressList from './UserAddressList.vue'
+import UserBasicInfo from './UserBasicInfo.vue'
+import UserBrokerageList from './UserBrokerageList.vue'
+import UserCouponList from './UserCouponList.vue'
+import UserExperienceRecordList from './UserExperienceRecordList.vue'
+import UserOrderList from './UserOrderList.vue'
+import UserPointList from './UserPointList.vue'
+import UserSignList from './UserSignList.vue'
+import UserFavoriteList from './UserFavoriteList.vue'
+import { CardTitle } from '@/components/Card/index'
+import { ElMessage } from 'element-plus'
+
+defineOptions({ name: 'MemberDetail' })
+
+const loading = ref(true) // 加载中
+const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string) => {
+  formRef.value.open(type, id)
+}
+
+/** 获得用户 */
+const getUserData = async (id: number) => {
+  loading.value = true
+  try {
+    user.value = await UserApi.getUser(id)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 */
+const { currentRoute } = useRouter() // 路由
+const { delView } = useTagsViewStore() // 视图操作
+const route = useRoute()
+const id = Number(route.params.id)
+onMounted(() => {
+  if (!id) {
+    ElMessage.warning('参数错误,会员编号不能为空!')
+    delView(unref(currentRoute))
+    return
+  }
+  getUserData(id)
+})
+</script>
+<style scoped lang="css">
+.detail-info-item:first-child {
+  padding-left: 0 !important;
+}
+
+/* first-child 不生效有没有大佬给看下q.q */
+.detail-info-item:nth-child(2) {
+  padding-right: 0 !important;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 295 - 0
src/views/coach/user/index.vue

@@ -0,0 +1,295 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="技工姓名" prop="nickname">
+        <el-input
+          v-model="queryParams.nickname"
+          class="!w-240px"
+          clearable
+          placeholder="请输入技工姓名"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="手机号" prop="mobile">
+        <el-input
+          v-model="queryParams.mobile"
+          class="!w-240px"
+          clearable
+          placeholder="请输入手机号"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="经纪人" prop="tagIds">
+        <MemberTagSelect v-model="queryParams.tagIds" />
+      </el-form-item>
+      <el-form-item label="代理商" prop="levelId">
+        <MemberLevelSelect v-model="queryParams.levelId" />
+      </el-form-item>
+      <el-form-item label="认证状态" prop="groupId">
+        <MemberGroupSelect v-model="queryParams.groupId" />
+      </el-form-item>
+      <el-form-item label="申请时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </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 v-hasPermi="['promotion:coupon:send']" @click="openCoupon">发送优惠券</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      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="id" width="120px" />
+      <el-table-column align="center" label="头像" prop="avatar" width="80px">
+        <template #default="scope">
+          <img :src="scope.row.avatar" style="width: 40px" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="姓名" prop="name" width="80px" />
+      <el-table-column align="center" label="手机号" prop="mobile" width="120px" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="申请时间"
+        prop="createTime"
+        width="180px"
+      />
+      <el-table-column align="center" label="申请来源" prop="levelName" width="100px" />
+      <el-table-column align="center" label="是否绑定" prop="groupName" width="100px" />
+      <el-table-column
+        :show-overflow-tooltip="false"
+        align="center"
+        label="认证状态"
+        prop="tagNames"
+      >
+        <template #default="scope">
+          <el-tag v-for="(tagName, index) in scope.row.tagNames" :key="index" class="mr-5px">
+            {{ tagName }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="状态" prop="status" width="100px">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="所属经纪人" prop="point" width="100px" />
+      <el-table-column align="center" label="所属代理商" prop="point" width="100px" />
+      <el-table-column align="center" label="挂靠门店" prop="point" width="100px" />
+      <el-table-column
+        :show-overflow-tooltip="false"
+        align="center"
+        fixed="right"
+        label="操作"
+        width="100px"
+      >
+        <template #default="scope">
+          <div class="flex items-center justify-center">
+            <el-button link type="primary" @click="openDetail(scope.row.id)">详情</el-button>
+            <el-dropdown
+              v-hasPermi="[
+                'member:user:update',
+                'member:user:update-level',
+                'member:user:update-point',
+                'member:user:update-balance'
+              ]"
+              @command="(command) => handleCommand(command, scope.row)"
+            >
+              <el-button link type="primary">
+                <Icon icon="ep:d-arrow-right" />
+                更多
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item
+                    v-if="checkPermi(['member:user:update'])"
+                    command="handleUpdate"
+                  >
+                    编辑
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    v-if="checkPermi(['member:user:update-level'])"
+                    command="handleUpdateLevel"
+                  >
+                    修改等级
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    v-if="checkPermi(['member:user:update-point'])"
+                    command="handleUpdatePoint"
+                  >
+                    修改积分
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    v-if="checkPermi(['member:user:update-balance'])"
+                    command="handleUpdateBlance"
+                  >
+                    修改余额(WIP)
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <UserForm ref="formRef" @success="getList" />
+  <!-- 修改用户等级弹窗 -->
+  <UserLevelUpdateForm ref="updateLevelFormRef" @success="getList" />
+  <!-- 修改用户积分弹窗 -->
+  <UserPointUpdateForm ref="updatePointFormRef" @success="getList" />
+  <!-- 发送优惠券弹窗 -->
+<!--  <CouponSendForm ref="couponSendFormRef" />-->
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as UserApi from '@/api/member/user'
+import { DICT_TYPE } from '@/utils/dict'
+import UserForm from './UserForm.vue'
+import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
+import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
+import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
+import UserLevelUpdateForm from './UserLevelUpdateForm.vue'
+import UserPointUpdateForm from './UserPointUpdateForm.vue'
+// import { CouponSendForm } from '@/views/mall/promotion/coupon/components'
+import { checkPermi } from '@/utils/permission'
+
+defineOptions({ name: 'MemberUser' })
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  nickname: null,
+  mobile: null,
+  loginDate: [],
+  createTime: [],
+  tagIds: [],
+  levelId: null,
+  groupId: null
+})
+const queryFormRef = ref() // 搜索的表单
+const updateLevelFormRef = ref() // 修改会员等级表单
+const updatePointFormRef = ref() // 修改会员积分表单
+const selectedIds = ref<number[]>([]) // 表格的选中 ID 数组
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await UserApi.getUserPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 打开会员详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'MemberUserDetail', params: { id } })
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 表格选中事件 */
+const handleSelectionChange = (rows: UserApi.UserVO[]) => {
+  selectedIds.value = rows.map((row) => row.id)
+}
+
+/** 发送优惠券 */
+const couponSendFormRef = ref()
+const openCoupon = () => {
+  if (selectedIds.value.length === 0) {
+    message.warning('请选择要发送优惠券的用户')
+    return
+  }
+  couponSendFormRef.value.open(selectedIds.value)
+}
+
+/** 操作分发 */
+const handleCommand = (command: string, row: UserApi.UserVO) => {
+  switch (command) {
+    case 'handleUpdate':
+      openForm('update', row.id)
+      break
+    case 'handleUpdateLevel':
+      updateLevelFormRef.value.open(row.id)
+      break
+    case 'handleUpdatePoint':
+      updatePointFormRef.value.open(row.id)
+      break
+    case 'handleUpdateBlance':
+      // todo @jason:增加一个【修改余额】
+      break
+    default:
+      break
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 122 - 0
src/views/service/category/UserForm.vue

@@ -0,0 +1,122 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="100px"
+        v-loading="formLoading"
+    >
+      <el-form-item label="分类名称" prop="title">
+        <el-input v-model="formData.title" maxlength="15" show-word-limit placeholder="请输入服务名称"/>
+      </el-form-item>
+      <el-form-item label="封面图" prop="cover">
+        <UploadImg v-model="formData.cover" :limit="1" :is-show-tip="false"/>
+      </el-form-item>
+      <el-form-item label="排序值" prop="sort">
+        <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="formData.status" clearable placeholder="请选择状态">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import {DICT_TYPE, getIntDictOptions} from '@/utils/dict'
+import * as CategoryApi from '@/api/service/category'
+import * as AreaApi from '@/api/system/area'
+import {defaultProps} from '@/utils/tree'
+import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
+import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
+import {CommonStatusEnum} from "@/utils/constants";
+
+const {t} = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  title: undefined,
+  cover: undefined,
+  sort: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  title: [{required: true, message: '分类名称不能为空', trigger: 'blur'}],
+  status: [{required: true, message: '状态不能为空', trigger: 'blur'}]
+})
+const formRef = ref() // 表单 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 CategoryApi.getCategory(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+
+}
+defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as CategoryApi.Category
+    if (formType.value === 'create') {
+      // 说明:目前暂时没有新增操作。如果自己业务需要,可以进行扩展
+      await CategoryApi.createCategory(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await CategoryApi.updateCategory(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    title: undefined,
+    cover: undefined,
+    sort: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 101 - 0
src/views/service/category/UserLevelUpdateForm.vue

@@ -0,0 +1,101 @@
+<template>
+  <Dialog title="修改用户等级" v-model="dialogVisible" width="600">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="用户编号" prop="id">
+        <el-input v-model="formData.id" placeholder="请输入用户昵称" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickname">
+        <el-input
+          v-model="formData.nickname"
+          placeholder="请输入用户昵称"
+          class="!w-240px"
+          disabled
+        />
+      </el-form-item>
+      <el-form-item label="用户等级" prop="levelId">
+        <MemberLevelSelect v-model="formData.levelId" />
+      </el-form-item>
+      <el-form-item label="修改原因" prop="reason">
+        <el-input type="textarea" v-model="formData.reason" placeholder="请输入修改原因" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  nickname: undefined,
+  levelId: undefined,
+  reason: undefined
+})
+const formRules = reactive({
+  reason: [{ required: true, message: '修改原因不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  dialogVisible.value = true
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await UserApi.getUser(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    await UserApi.updateUserLevel(formData.value)
+
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    nickname: undefined,
+    levelId: undefined,
+    reason: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 128 - 0
src/views/service/category/UserPointUpdateForm.vue

@@ -0,0 +1,128 @@
+<template>
+  <Dialog title="修改用户积分" v-model="dialogVisible" width="600">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="用户编号" prop="id">
+        <el-input v-model="formData.id" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickname">
+        <el-input v-model="formData.nickname" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="变动前积分" prop="point">
+        <el-input-number v-model="formData.point" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="变动类型" prop="changeType">
+        <el-radio-group v-model="formData.changeType">
+          <el-radio :label="1">增加</el-radio>
+          <el-radio :label="-1">减少</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="变动积分" prop="changePoint">
+        <el-input-number v-model="formData.changePoint" class="!w-240px" :min="0" :precision="0" />
+      </el-form-item>
+      <el-form-item label="变动后积分">
+        <el-input-number v-model="pointResult" class="!w-240px" disabled />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+
+/** 修改用户积分表单 */
+defineOptions({ name: 'UpdatePointForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  nickname: undefined,
+  point: 0,
+  changePoint: 0,
+  changeType: 1
+})
+const formRules = reactive({
+  changePoint: [{ required: true, message: '变动积分不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  dialogVisible.value = true
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await UserApi.getUser(id)
+      formData.value.changeType = 1 // 默认增加积分
+      formData.value.changePoint = 0 // 变动积分默认0
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+
+  if (formData.value.changePoint < 1) {
+    message.error('变动积分不能小于 1')
+    return
+  }
+  if (pointResult.value < 0) {
+    message.error('变动后的积分不能小于 0')
+    return
+  }
+
+  // 提交请求
+  formLoading.value = true
+  try {
+    await UserApi.updateUserPoint({
+      id: formData.value.id,
+      point: formData.value.changePoint * formData.value.changeType
+    })
+
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    nickname: undefined,
+    levelId: undefined,
+    reason: undefined
+  }
+  formRef.value?.resetFields()
+}
+
+/** 变动后的积分 */
+const pointResult = computed(
+  () => formData.value.point + formData.value.changePoint * formData.value.changeType
+)
+</script>

+ 14 - 0
src/views/service/category/components/balance-list.vue

@@ -0,0 +1,14 @@
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  name: 'BalanceList'
+})
+</script>
+
+<!-- TODO @芋艿:未来实现,等周建的 -->
+<template>
+  <div>余额列表</div>
+</template>
+
+<style scoped lang="scss"></style>

+ 87 - 0
src/views/service/category/detail/UserAccountInfo.vue

@@ -0,0 +1,87 @@
+<template>
+  <el-descriptions :column="2">
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 等级 " icon="svg-icon:member_level" />
+      </template>
+      {{ user.levelName || '无' }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 成长值 " icon="ep:suitcase" />
+      </template>
+      {{ user.experience || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 当前积分 " icon="ep:coin" />
+      </template>
+      {{ user.point || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 总积分 " icon="ep:coin" />
+      </template>
+      {{ user.totalPoint || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 当前余额 " icon="svg-icon:member_balance" />
+      </template>
+      {{ fenToYuan(wallet.balance || 0) }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 支出金额 " icon="svg-icon:member_expenditure_balance" />
+      </template>
+      {{ fenToYuan(wallet.totalExpense || 0) }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 充值金额 " icon="svg-icon:member_recharge_balance" />
+      </template>
+      {{ fenToYuan(wallet.totalRecharge || 0) }}
+    </el-descriptions-item>
+  </el-descriptions>
+</template>
+<script setup lang="ts">
+import { DescriptionsItemLabel } from '@/components/Descriptions'
+import * as UserApi from '@/api/member/user'
+import * as WalletApi from '@/api/pay/wallet/balance'
+import { UserTypeEnum } from '@/utils/constants'
+import { fenToYuan } from '@/utils'
+
+const props = defineProps<{ user: UserApi.UserVO }>() // 用户信息
+const WALLET_INIT_DATA = {
+  balance: 0,
+  totalExpense: 0,
+  totalRecharge: 0
+} as WalletApi.WalletVO // 钱包初始化数据
+const wallet = ref<WalletApi.WalletVO>(WALLET_INIT_DATA) // 钱包信息
+
+/** 查询用户钱包信息 */
+const getUserWallet = async () => {
+  if (!props.user.id) {
+    wallet.value = WALLET_INIT_DATA
+    return
+  }
+  const params = { userId: props.user.id }
+  wallet.value = (await WalletApi.getWallet(params)) || WALLET_INIT_DATA
+}
+
+/** 监听用户编号变化 */
+watch(
+  () => props.user.id,
+  () => getUserWallet(),
+  { immediate: true }
+)
+</script>
+<style scoped lang="scss">
+.cell-item {
+  display: inline;
+}
+
+.cell-item::after {
+  content: ':';
+}
+</style>

+ 54 - 0
src/views/service/category/detail/UserAddressList.vue

@@ -0,0 +1,54 @@
+<template>
+  <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+    <el-table-column label="地址编号" align="center" prop="id" width="150px" />
+    <el-table-column label="收件人名称" align="center" prop="name" width="150px" />
+    <el-table-column label="手机号" align="center" prop="mobile" width="150px" />
+    <el-table-column label="地区编码" align="center" prop="areaId" width="150px" />
+    <el-table-column label="收件详细地址" align="center" prop="detailAddress" />
+    <el-table-column label="是否默认" align="center" prop="defaultStatus" width="150px">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="Number(scope.row.defaultStatus)" />
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="创建时间"
+      align="center"
+      prop="createTime"
+      :formatter="dateFormatter"
+      width="180px"
+    />
+  </el-table>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as AddressApi from '@/api/member/address'
+
+const { userId }: { userId: number } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await AddressApi.getAddressList({ userId })
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 85 - 0
src/views/service/category/detail/UserBasicInfo.vue

@@ -0,0 +1,85 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <slot name="header"></slot>
+    </template>
+    <el-row >
+      <el-col :span="5">
+        <ElAvatar shape="square" :size="140" :src="user.avatar || undefined" />
+      </el-col>
+      <el-col :span="19">
+        <el-descriptions :column="2">
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="用户名" icon="ep:user" />
+            </template>
+            {{ user.name || '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="昵称" icon="ep:user" />
+            </template>
+            {{ user.nickname }}
+          </el-descriptions-item>
+          <el-descriptions-item label="手机号">
+            <template #label>
+              <descriptions-item-label label="手机号" icon="ep:phone" />
+            </template>
+            {{ user.mobile }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="性别" icon="fa:mars-double" />
+            </template>
+            <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="user.sex" />
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="所在地" icon="ep:location" />
+            </template>
+            {{ user.areaName }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="注册 IP" icon="ep:position" />
+            </template>
+            {{ user.registerIp }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="生日" icon="fa:birthday-cake" />
+            </template>
+            {{ user.birthday ? formatDate(user.birthday) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="注册时间" icon="ep:calendar" />
+            </template>
+            {{ user.createTime ? formatDate(user.createTime) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="最后登录时间" icon="ep:calendar" />
+            </template>
+            {{ user.loginDate ? formatDate(user.loginDate) : '空' }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-col>
+    </el-row>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as UserApi from '@/api/member/user'
+import { DescriptionsItemLabel } from '@/components/Descriptions/index'
+
+const { user } = defineProps<{ user: UserApi.UserVO }>()
+</script>
+<style scoped lang="scss">
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 125 - 0
src/views/service/category/detail/UserBrokerageList.vue

@@ -0,0 +1,125 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="85px"
+    >
+      <el-form-item label="用户类型" prop="level">
+        <el-radio-group v-model="queryParams.level" @change="handleQuery">
+          <el-radio-button checked>全部</el-radio-button>
+          <el-radio-button label="1">一级推广人</el-radio-button>
+          <el-radio-button label="2">二级推广人</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="绑定时间" prop="bindUserTime">
+        <el-date-picker
+          v-model="queryParams.bindUserTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="用户编号" align="center" prop="id" min-width="80px" />
+      <el-table-column label="头像" align="center" prop="avatar" width="70px">
+        <template #default="scope">
+          <el-avatar :src="scope.row.avatar" />
+        </template>
+      </el-table-column>
+      <el-table-column label="昵称" align="center" prop="nickname" min-width="80px" />
+      <el-table-column label="等级" align="center" prop="level" min-width="80px">
+        <template #default="scope">
+          <el-tag v-if="scope.row.bindUserId === bindUserId">一级</el-tag>
+          <el-tag v-else>二级</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="绑定时间"
+        align="center"
+        prop="bindUserTime"
+        :formatter="dateFormatter"
+        width="170px"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+
+/** 推广人列表 */
+defineOptions({ name: 'UserBrokerageList' })
+
+const { bindUserId }: { bindUserId: number } = defineProps({
+  bindUserId: {
+    type: Number,
+    required: true
+  }
+}) //用户编号
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  bindUserId: null,
+  level: '',
+  bindUserTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.bindUserId = bindUserId
+    const data = await BrokerageUserApi.getBrokerageUserPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 190 - 0
src/views/service/category/detail/UserCouponList.vue

@@ -0,0 +1,190 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />搜索 </el-button>
+        <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <!-- Tab 选项:真正的内容在 Lab -->
+    <el-tabs v-model="activeTab" type="card" @tab-change="onTabChange">
+      <el-tab-pane
+        v-for="tab in statusTabs"
+        :key="tab.value"
+        :label="tab.label"
+        :name="tab.value"
+      />
+    </el-tabs>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="优惠劵" align="center" prop="name" />
+      <el-table-column label="优惠券类型" align="center" prop="discountType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="领取方式" align="center" prop="takeType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="领取时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column
+        label="使用时间"
+        align="center"
+        prop="useTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['promotion:coupon:delete']"
+            type="danger"
+            link
+            @click="handleDelete(scope.row.id)"
+          >
+            回收
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts" name="UserCouponList">
+import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon/coupon'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'UserCouponList' })
+
+const { userId }: { userId: number } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+}) //用户编号
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 字典表格数据
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  createTime: [],
+  status: undefined,
+  userIds: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+const activeTab = ref('all') // Tab 筛选
+const statusTabs = reactive([
+  {
+    label: '全部',
+    value: 'all'
+  }
+])
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  // 执行查询
+  try {
+    queryParams.userIds = userId
+    const data = await getCouponPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 二次确认
+    await message.confirm(
+      '回收将会收回会员领取的待使用的优惠券,已使用的将无法回收,确定要回收所选优惠券吗?'
+    )
+    // 发起删除
+    await deleteCoupon(id)
+    message.notifySuccess('回收成功')
+    // 重新加载列表
+    await getList()
+  } catch {}
+}
+
+/** tab 切换 */
+const onTabChange = (tabName) => {
+  queryParams.status = tabName === 'all' ? undefined : tabName
+  getList()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+  // 设置 statuses 过滤
+  for (const dict of getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_STATUS)) {
+    statusTabs.push({
+      label: dict.label,
+      value: dict.value as string
+    })
+  }
+})
+</script>

+ 158 - 0
src/views/service/category/detail/UserExperienceRecordList.vue

@@ -0,0 +1,158 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="业务类型" prop="bizType">
+        <el-select
+          v-model="queryParams.bizType"
+          placeholder="请选择业务类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="标题" prop="title">
+        <el-input
+          v-model="queryParams.title"
+          placeholder="请输入标题"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" width="150px" />
+      <el-table-column
+        label="获得时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="经验" align="center" prop="experience" width="150px">
+        <template #default="scope">
+          <el-tag v-if="scope.row.experience > 0" class="ml-2" type="success" effect="dark">
+            +{{ scope.row.experience }}
+          </el-tag>
+          <el-tag v-else class="ml-2" type="danger" effect="dark">
+            {{ scope.row.experience }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="总经验" align="center" prop="totalExperience" width="150px">
+        <template #default="scope">
+          <el-tag class="ml-2" effect="dark">
+            {{ scope.row.totalExperience }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="标题" align="center" prop="title" width="150px" />
+      <el-table-column label="描述" align="center" prop="description" />
+      <el-table-column label="业务编号" align="center" prop="bizId" width="150px" />
+      <el-table-column label="业务类型" align="center" prop="bizType" width="150px">
+        <!--   TODO 芋艿:此处应创建对应的字典 -->
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE" :value="scope.row.bizType" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as ExperienceRecordApi from '@/api/member/experience-record/index'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'UserExperienceRecordList' })
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: null,
+  bizId: null,
+  bizType: null,
+  title: null,
+  description: null,
+  experience: null,
+  totalExperience: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ExperienceRecordApi.getExperienceRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 96 - 0
src/views/service/category/detail/UserFavoriteList.vue

@@ -0,0 +1,96 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column key="id" align="center" label="商品编号" width="180" prop="id" />
+      <el-table-column label="商品图" min-width="80">
+        <template #default="{ row }">
+          <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+        </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 }"> {{ floatToFixed2(row.price) }}元</template>
+      </el-table-column>
+      <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="收藏时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="状态" min-width="80">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PRODUCT_SPU_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as FavoriteApi from '@/api/mall/product/favorite'
+import { floatToFixed2 } from '@/utils'
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  createTime: [],
+  userId: NaN
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await FavoriteApi.getFavoritePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 279 - 0
src/views/service/category/detail/UserOrderList.vue

@@ -0,0 +1,279 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="订单状态" prop="status">
+        <el-select v-model="queryParams.status" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="支付方式" prop="payChannelCode">
+        <el-select
+          v-model="queryParams.payChannelCode"
+          class="!w-280px"
+          clearable
+          placeholder="全部"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-280px"
+          end-placeholder="自定义时间"
+          start-placeholder="自定义时间"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="订单来源" prop="terminal">
+        <el-select v-model="queryParams.terminal" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TERMINAL)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="订单类型" prop="type">
+        <el-select v-model="queryParams.type" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="配送方式" prop="deliveryType">
+        <el-select v-model="queryParams.deliveryType" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+        label="快递公司"
+        prop="logisticsId"
+      >
+        <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="item in deliveryExpressList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+        label="自提门店"
+        prop="pickUpStoreId"
+      >
+        <el-select
+          v-model="queryParams.pickUpStoreId"
+          class="!w-280px"
+          clearable
+          multiple
+          placeholder="全部"
+        >
+          <el-option
+            v-for="item in pickUpStoreList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+        label="核销码"
+        prop="pickUpVerifyCode"
+      >
+        <el-input
+          v-model="queryParams.pickUpVerifyCode"
+          class="!w-280px"
+          clearable
+          placeholder="请输入自提核销码"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="聚合搜索">
+        <el-input
+          v-show="true"
+          v-model="queryParams[queryType.queryParam]"
+          class="!w-280px"
+          clearable
+          placeholder="请输入"
+        >
+          <template #prepend>
+            <el-select
+              v-model="queryType.queryParam"
+              class="!w-110px"
+              clearable
+              placeholder="全部"
+              @change="inputChangeSelect"
+            >
+              <el-option
+                v-for="dict in dynamicSearchList"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-input>
+      </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-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <!-- 添加 row-key="id" 解决列数据中的 table#header 数据不刷新的问题  -->
+    <el-table v-loading="loading" :data="list" row-key="id">
+<!--      <OrderTableColumn :list="list" :pick-up-store-list="pickUpStoreList">-->
+<!--        <template #default="{ row }">-->
+<!--          <el-button link type="primary" @click="openDetail(row.id)">-->
+<!--            <Icon icon="ep:notification" />-->
+<!--            详情-->
+<!--          </el-button>-->
+<!--        </template>-->
+<!--      </OrderTableColumn>-->
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as OrderApi from '@/api/mall/trade/order/index'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import { FormInstance } from 'element-plus'
+// import { OrderTableColumn } from '@/views/mall/trade/order/components'
+import { DeliveryTypeEnum } from '@/utils/constants'
+
+const { push } = useRouter() // 路由跳转
+
+const { userId } = defineProps<{
+  userId: number
+}>()
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const pickUpStoreList = ref<PickUpStoreApi.DeliveryPickUpStoreVO[]>([]) // 自提门店精简列表
+const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 物流公司
+const queryFormRef = ref<FormInstance>() // 搜索的表单
+// 表单搜索
+const queryParams = ref({
+  pageNo: 1, // 页数
+  pageSize: 10, // 每页显示数量
+  userId: userId,
+  status: undefined, // 订单状态
+  payChannelCode: undefined, // 支付方式
+  createTime: undefined, // 创建时间
+  terminal: undefined, // 订单来源
+  type: undefined, // 订单类型
+  deliveryType: undefined, // 配送方式
+  logisticsId: undefined, // 快递公司
+  pickUpStoreId: undefined, // 自提门店
+  pickUpVerifyCode: undefined // 自提核销码
+})
+const queryType = reactive({ queryParam: '' }) // 订单搜索类型 queryParam
+
+// 订单聚合搜索 select 类型配置(动态搜索)
+const dynamicSearchList = ref([
+  { value: 'no', label: '订单号' },
+  { value: 'userNickname', label: '用户昵称' },
+  { value: 'userMobile', label: '用户电话' }
+])
+/**
+ * 聚合搜索切换查询对象时触发
+ * @param val
+ */
+const inputChangeSelect = (val: string) => {
+  dynamicSearchList.value
+    .filter((item) => item.value !== val)
+    ?.forEach((item1) => {
+      // 清除集合搜索无用属性
+      if (queryParams.value.hasOwnProperty(item1.value)) {
+        delete queryParams.value[item1.value]
+      }
+    })
+}
+
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  queryParams.value.pageNo = 1
+  await getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.value.userId = userId
+  handleQuery()
+}
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await OrderApi.getOrderPage(queryParams.value)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 查看订单详情 */
+const openDetail = (id: number) => {
+  push({ name: 'TradeOrderDetail', params: { id } })
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+  deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+})
+</script>

+ 152 - 0
src/views/service/category/detail/UserPointList.vue

@@ -0,0 +1,152 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="业务类型" prop="bizType">
+        <el-select
+          v-model="queryParams.bizType"
+          placeholder="请选择业务类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_POINT_BIZ_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="积分标题" prop="title">
+        <el-input
+          v-model="queryParams.title"
+          placeholder="请输入积分标题"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="获得时间" prop="createDate">
+        <el-date-picker
+          v-model="queryParams.createDate"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" width="180" />
+      <el-table-column
+        label="获得时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column label="获得积分" align="center" prop="point" width="100">
+        <template #default="scope">
+          <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+            +{{ scope.row.point }}
+          </el-tag>
+          <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="总积分" align="center" prop="totalPoint" width="100" />
+      <el-table-column label="标题" align="center" prop="title" />
+      <el-table-column label="描述" align="center" prop="description" />
+      <el-table-column label="业务编码" align="center" prop="bizId" />
+      <el-table-column label="业务类型" align="center" prop="bizType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.MEMBER_POINT_BIZ_TYPE" :value="scope.row.bizType" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as RecordApi from '@/api//member/point/record'
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  bizType: undefined,
+  title: null,
+  createDate: [],
+  userId: NaN
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await RecordApi.getRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 135 - 0
src/views/service/category/detail/UserSignList.vue

@@ -0,0 +1,135 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="签到用户" prop="nickname">
+        <el-input
+          v-model="queryParams.nickname"
+          placeholder="请输入签到用户"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="签到天数" prop="day">
+        <el-input
+          v-model="queryParams.day"
+          placeholder="请输入签到天数"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="签到时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column
+        label="签到天数"
+        align="center"
+        prop="day"
+        :formatter="(_, __, cellValue) => ['第', cellValue, '天'].join(' ')"
+      />
+      <el-table-column label="获得积分" align="center" prop="point" width="100">
+        <template #default="scope">
+          <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+            +{{ scope.row.point }}
+          </el-tag>
+          <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="签到时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as SignInRecordApi from '@/api/member/signin/record'
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: NaN,
+  nickname: null,
+  day: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SignInRecordApi.getSignInRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 135 - 0
src/views/service/category/detail/index.vue

@@ -0,0 +1,135 @@
+<template>
+  <div v-loading="loading">
+    <el-row :gutter="10">
+      <!-- 左上角:基本信息 -->
+      <el-col :span="14" class="detail-info-item">
+        <UserBasicInfo :user="user">
+          <template #header>
+            <div class="card-header">
+              <CardTitle title="基本信息" />
+              <el-button type="primary" size="small" text @click="openForm('update')">
+                编辑
+              </el-button>
+            </div>
+          </template>
+        </UserBasicInfo>
+      </el-col>
+      <!-- 右上角:账户信息 -->
+      <el-col :span="10" class="detail-info-item">
+        <el-card shadow="never" class="h-full">
+          <template #header>
+            <CardTitle title="账户信息" />
+          </template>
+          <UserAccountInfo :user="user" />
+        </el-card>
+      </el-col>
+      <!-- 下边:账户明细 -->
+      <!-- TODO 芋艿:【订单管理】【售后管理】【收藏记录】-->
+      <el-card header="账户明细" style="width: 100%; margin-top: 20px" shadow="never">
+        <template #header>
+          <CardTitle title="账户明细" />
+        </template>
+        <el-tabs>
+          <el-tab-pane label="积分">
+            <UserPointList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="签到" lazy>
+            <UserSignList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="成长值" lazy>
+            <UserExperienceRecordList :user-id="id" />
+          </el-tab-pane>
+          <!-- TODO @jason:增加一个余额变化; -->
+          <el-tab-pane label="余额" lazy>余额(WIP)</el-tab-pane>
+          <el-tab-pane label="收货地址" lazy>
+            <UserAddressList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="订单管理" lazy>
+            <UserOrderList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="售后管理" lazy>售后管理(WIP)</el-tab-pane>
+          <el-tab-pane label="收藏记录" lazy>
+            <UserFavoriteList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="优惠劵" lazy>
+            <UserCouponList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="推广用户" lazy>
+            <UserBrokerageList :bind-user-id="id" />
+          </el-tab-pane>
+        </el-tabs>
+      </el-card>
+    </el-row>
+  </div>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <UserForm ref="formRef" @success="getUserData(id)" />
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import UserForm from '@/views/member/user/UserForm.vue'
+import UserAccountInfo from './UserAccountInfo.vue'
+import UserAddressList from './UserAddressList.vue'
+import UserBasicInfo from './UserBasicInfo.vue'
+import UserBrokerageList from './UserBrokerageList.vue'
+import UserCouponList from './UserCouponList.vue'
+import UserExperienceRecordList from './UserExperienceRecordList.vue'
+import UserOrderList from './UserOrderList.vue'
+import UserPointList from './UserPointList.vue'
+import UserSignList from './UserSignList.vue'
+import UserFavoriteList from './UserFavoriteList.vue'
+import { CardTitle } from '@/components/Card/index'
+import { ElMessage } from 'element-plus'
+
+defineOptions({ name: 'MemberDetail' })
+
+const loading = ref(true) // 加载中
+const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string) => {
+  formRef.value.open(type, id)
+}
+
+/** 获得用户 */
+const getUserData = async (id: number) => {
+  loading.value = true
+  try {
+    user.value = await UserApi.getUser(id)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 */
+const { currentRoute } = useRouter() // 路由
+const { delView } = useTagsViewStore() // 视图操作
+const route = useRoute()
+const id = Number(route.params.id)
+onMounted(() => {
+  if (!id) {
+    ElMessage.warning('参数错误,会员编号不能为空!')
+    delView(unref(currentRoute))
+    return
+  }
+  getUserData(id)
+})
+</script>
+<style scoped lang="css">
+.detail-info-item:first-child {
+  padding-left: 0 !important;
+}
+
+/* first-child 不生效有没有大佬给看下q.q */
+.detail-info-item:nth-child(2) {
+  padding-right: 0 !important;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 205 - 0
src/views/service/category/index.vue

@@ -0,0 +1,205 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="分类名称" prop="title">
+        <el-input
+          v-model="queryParams.title"
+          class="!w-240px"
+          clearable
+          placeholder="请输入分类名称"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="请选择状态">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="注册时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </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
+          v-hasPermi="['system:role:create']"
+          plain
+          type="primary"
+          @click="openForm('create')"
+        >
+          <Icon class="mr-5px" icon="ep:plus"/>
+          新增
+        </el-button>
+      </el-form-item>
+
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="编号" prop="id" />
+      <el-table-column align="center" label="封面图" prop="cover" >
+        <template #default="scope">
+          <img :src="scope.row.cover" style="width: 40px" alt=""/>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="分类名称" prop="title" />
+      <el-table-column align="center" label="排序" prop="sort" />
+      <el-table-column align="center" label="状态" prop="status" >
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
+        width="180px"
+      />
+      <el-table-column
+        :show-overflow-tooltip="false"
+        align="center"
+        fixed="right"
+        label="操作"
+        width="200"
+      >
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['system:role:update']"
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+          >
+            编辑
+          </el-button>
+          <el-button
+            v-hasPermi="['system:role:delete']"
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <UserForm ref="formRef" @success="getList"/>
+
+</template>
+<script lang="ts" setup>
+import {dateFormatter} from '@/utils/formatTime'
+import * as CategoryApi from '@/api/service/category'
+import {DICT_TYPE, getIntDictOptions} from '@/utils/dict'
+import UserForm from './UserForm.vue'
+
+defineOptions({name: 'ServiceCategory'})
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  title: undefined,
+  status: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const selectedIds = ref<number[]>([]) // 表格的选中 ID 数组
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await CategoryApi.getCategoryList(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 表格选中事件 */
+const handleSelectionChange = (rows: CategoryApi.Category[]) => {
+  selectedIds.value = rows.map((row) => row.id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await CategoryApi.deleteCategory(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 231 - 0
src/views/service/project/UserForm.vue

@@ -0,0 +1,231 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+      v-loading="formLoading"
+    >
+      <el-row>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="服务封面" prop="cover">
+            <UploadImg v-model="formData.cover" :limit="1" :is-show-tip="false"/>
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="服务名称" prop="title">
+            <el-input v-model="formData.title" maxlength="15" show-word-limit
+                      placeholder="请输入服务名称"/>
+          </el-form-item>
+          <el-form-item label="副标题" prop="sub_title">
+            <el-input v-model="formData.subTitle" maxlength="30" show-word-limit
+                      placeholder="请输入服务名称"/>
+          </el-form-item>
+          <el-form-item label="所属分类" prop="category">
+            <el-select v-model="formData.category" clearable placeholder="请选择所属分类">
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+
+
+        <!--      <el-form-item label="轮播图" prop="avatar">-->
+        <!--        <UploadImg v-model="formData.avatar" :limit="1" :is-show-tip="false"/>-->
+        <!--      </el-form-item>-->
+
+
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="服务价格" prop="price">
+            <el-input v-model="formData.price" placeholder="请输入服务价格">
+              <template #append>元</template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="服务原价" prop="init_price">
+            <el-input v-model="formData.initPrice" placeholder="请输入服务原价">
+              <template #append>元</template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="物料费" prop="materialPrice">
+            <el-input v-model="formData.materialPrice" placeholder="请输入物料费">
+              <template #append>元</template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="虚拟销量" prop="sale">
+            <el-input v-model="formData.sale" placeholder="请输入虚拟销量">
+              <template #append>单</template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+
+          <el-form-item label="服务时长" prop="timeLong">
+            <el-input v-model="formData.timeLong" placeholder="请输入服务时长">
+              <template #append>分</template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="分佣比例" prop="comBalance">
+            <el-input v-model="formData.comBalance" placeholder="请输入分佣比例">
+              <template #append>%</template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="加钟服务" prop="isAdd">
+            <el-switch v-model="formData.isAdd"/>
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="支持到店" prop="isStore">
+            <el-switch v-model="formData.isStore"/>
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+          <el-form-item label="支持上门" prop="isDoor">
+            <el-switch v-model="formData.isDoor"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="排序值" prop="sort">
+        <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right"/>
+      </el-form-item>
+      <el-form-item label="项目介绍" prop="introduce">
+        <Editor :model-value="formData.introduce" height="150px"/>
+      </el-form-item>
+      <el-form-item label="禁忌说明" prop="explain">
+        <Editor :model-value="formData.explain" height="150px"/>
+      </el-form-item>
+      <el-form-item label="下单须知" prop="notice">
+        <Editor :model-value="formData.notice" height="150px"/>
+      </el-form-item>
+<!--      <el-form-item label="关联技工" prop="coach">-->
+<!--        <Editor :model-value="formData.coach" height="150px"/>-->
+<!--      </el-form-item>-->
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import {DICT_TYPE, getIntDictOptions} from '@/utils/dict'
+import * as ProjectApi from '@/api/service/project'
+
+const {t} = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  title: undefined,
+  subTitle: undefined,
+  category: [],
+  cover: undefined,
+  price: undefined,
+  initPrice: undefined,
+  materialPrice: undefined,
+  sale: undefined,
+  timeLong: undefined,
+  comBalance: undefined,
+  sort: undefined,
+  isStore: undefined,
+  isDoor: undefined,
+  isAdd: undefined,
+  introduce: undefined,
+  explain: undefined,
+  notice: undefined
+})
+const formRules = reactive({
+  title: [{required: true, message: '服务名称不能为空', trigger: 'blur'}],
+})
+const formRef = ref() // 表单 Ref
+const areaList = 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 ProjectApi.getProject(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  // 获得地区列表
+  // areaList.value = await AreaApi.getAreaTree()
+}
+defineExpose({open}) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ProjectApi.Project
+    if (formType.value === 'create') {
+      await ProjectApi.createProject(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ProjectApi.updateProject(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    title: undefined,
+    subTitle: undefined,
+    category: [],
+    cover: undefined,
+    price: undefined,
+    initPrice: undefined,
+    materialPrice: undefined,
+    sale: undefined,
+    timeLong: undefined,
+    comBalance: undefined,
+    sort: undefined,
+    isStore: true,
+    isDoor: true,
+    isAdd: false,
+    introduce: undefined,
+    explain: undefined,
+    notice: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 101 - 0
src/views/service/project/UserLevelUpdateForm.vue

@@ -0,0 +1,101 @@
+<template>
+  <Dialog title="修改用户等级" v-model="dialogVisible" width="600">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="用户编号" prop="id">
+        <el-input v-model="formData.id" placeholder="请输入用户昵称" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickname">
+        <el-input
+          v-model="formData.nickname"
+          placeholder="请输入用户昵称"
+          class="!w-240px"
+          disabled
+        />
+      </el-form-item>
+      <el-form-item label="用户等级" prop="levelId">
+        <MemberLevelSelect v-model="formData.levelId" />
+      </el-form-item>
+      <el-form-item label="修改原因" prop="reason">
+        <el-input type="textarea" v-model="formData.reason" placeholder="请输入修改原因" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  nickname: undefined,
+  levelId: undefined,
+  reason: undefined
+})
+const formRules = reactive({
+  reason: [{ required: true, message: '修改原因不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  dialogVisible.value = true
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await UserApi.getUser(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    await UserApi.updateUserLevel(formData.value)
+
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    nickname: undefined,
+    levelId: undefined,
+    reason: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 128 - 0
src/views/service/project/UserPointUpdateForm.vue

@@ -0,0 +1,128 @@
+<template>
+  <Dialog title="修改用户积分" v-model="dialogVisible" width="600">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="用户编号" prop="id">
+        <el-input v-model="formData.id" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickname">
+        <el-input v-model="formData.nickname" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="变动前积分" prop="point">
+        <el-input-number v-model="formData.point" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="变动类型" prop="changeType">
+        <el-radio-group v-model="formData.changeType">
+          <el-radio :label="1">增加</el-radio>
+          <el-radio :label="-1">减少</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="变动积分" prop="changePoint">
+        <el-input-number v-model="formData.changePoint" class="!w-240px" :min="0" :precision="0" />
+      </el-form-item>
+      <el-form-item label="变动后积分">
+        <el-input-number v-model="pointResult" class="!w-240px" disabled />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+
+/** 修改用户积分表单 */
+defineOptions({ name: 'UpdatePointForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  nickname: undefined,
+  point: 0,
+  changePoint: 0,
+  changeType: 1
+})
+const formRules = reactive({
+  changePoint: [{ required: true, message: '变动积分不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  dialogVisible.value = true
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await UserApi.getUser(id)
+      formData.value.changeType = 1 // 默认增加积分
+      formData.value.changePoint = 0 // 变动积分默认0
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+
+  if (formData.value.changePoint < 1) {
+    message.error('变动积分不能小于 1')
+    return
+  }
+  if (pointResult.value < 0) {
+    message.error('变动后的积分不能小于 0')
+    return
+  }
+
+  // 提交请求
+  formLoading.value = true
+  try {
+    await UserApi.updateUserPoint({
+      id: formData.value.id,
+      point: formData.value.changePoint * formData.value.changeType
+    })
+
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    nickname: undefined,
+    levelId: undefined,
+    reason: undefined
+  }
+  formRef.value?.resetFields()
+}
+
+/** 变动后的积分 */
+const pointResult = computed(
+  () => formData.value.point + formData.value.changePoint * formData.value.changeType
+)
+</script>

+ 14 - 0
src/views/service/project/components/balance-list.vue

@@ -0,0 +1,14 @@
+<script lang="ts">
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  name: 'BalanceList'
+})
+</script>
+
+<!-- TODO @芋艿:未来实现,等周建的 -->
+<template>
+  <div>余额列表</div>
+</template>
+
+<style scoped lang="scss"></style>

+ 87 - 0
src/views/service/project/detail/UserAccountInfo.vue

@@ -0,0 +1,87 @@
+<template>
+  <el-descriptions :column="2">
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 等级 " icon="svg-icon:member_level" />
+      </template>
+      {{ user.levelName || '无' }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 成长值 " icon="ep:suitcase" />
+      </template>
+      {{ user.experience || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 当前积分 " icon="ep:coin" />
+      </template>
+      {{ user.point || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 总积分 " icon="ep:coin" />
+      </template>
+      {{ user.totalPoint || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 当前余额 " icon="svg-icon:member_balance" />
+      </template>
+      {{ fenToYuan(wallet.balance || 0) }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 支出金额 " icon="svg-icon:member_expenditure_balance" />
+      </template>
+      {{ fenToYuan(wallet.totalExpense || 0) }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 充值金额 " icon="svg-icon:member_recharge_balance" />
+      </template>
+      {{ fenToYuan(wallet.totalRecharge || 0) }}
+    </el-descriptions-item>
+  </el-descriptions>
+</template>
+<script setup lang="ts">
+import { DescriptionsItemLabel } from '@/components/Descriptions'
+import * as UserApi from '@/api/member/user'
+import * as WalletApi from '@/api/pay/wallet/balance'
+import { UserTypeEnum } from '@/utils/constants'
+import { fenToYuan } from '@/utils'
+
+const props = defineProps<{ user: UserApi.UserVO }>() // 用户信息
+const WALLET_INIT_DATA = {
+  balance: 0,
+  totalExpense: 0,
+  totalRecharge: 0
+} as WalletApi.WalletVO // 钱包初始化数据
+const wallet = ref<WalletApi.WalletVO>(WALLET_INIT_DATA) // 钱包信息
+
+/** 查询用户钱包信息 */
+const getUserWallet = async () => {
+  if (!props.user.id) {
+    wallet.value = WALLET_INIT_DATA
+    return
+  }
+  const params = { userId: props.user.id }
+  wallet.value = (await WalletApi.getWallet(params)) || WALLET_INIT_DATA
+}
+
+/** 监听用户编号变化 */
+watch(
+  () => props.user.id,
+  () => getUserWallet(),
+  { immediate: true }
+)
+</script>
+<style scoped lang="scss">
+.cell-item {
+  display: inline;
+}
+
+.cell-item::after {
+  content: ':';
+}
+</style>

+ 54 - 0
src/views/service/project/detail/UserAddressList.vue

@@ -0,0 +1,54 @@
+<template>
+  <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+    <el-table-column label="地址编号" align="center" prop="id" width="150px" />
+    <el-table-column label="收件人名称" align="center" prop="name" width="150px" />
+    <el-table-column label="手机号" align="center" prop="mobile" width="150px" />
+    <el-table-column label="地区编码" align="center" prop="areaId" width="150px" />
+    <el-table-column label="收件详细地址" align="center" prop="detailAddress" />
+    <el-table-column label="是否默认" align="center" prop="defaultStatus" width="150px">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="Number(scope.row.defaultStatus)" />
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="创建时间"
+      align="center"
+      prop="createTime"
+      :formatter="dateFormatter"
+      width="180px"
+    />
+  </el-table>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as AddressApi from '@/api/member/address'
+
+const { userId }: { userId: number } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await AddressApi.getAddressList({ userId })
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 85 - 0
src/views/service/project/detail/UserBasicInfo.vue

@@ -0,0 +1,85 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <slot name="header"></slot>
+    </template>
+    <el-row >
+      <el-col :span="5">
+        <ElAvatar shape="square" :size="140" :src="user.avatar || undefined" />
+      </el-col>
+      <el-col :span="19">
+        <el-descriptions :column="2">
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="用户名" icon="ep:user" />
+            </template>
+            {{ user.name || '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="昵称" icon="ep:user" />
+            </template>
+            {{ user.nickname }}
+          </el-descriptions-item>
+          <el-descriptions-item label="手机号">
+            <template #label>
+              <descriptions-item-label label="手机号" icon="ep:phone" />
+            </template>
+            {{ user.mobile }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="性别" icon="fa:mars-double" />
+            </template>
+            <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="user.sex" />
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="所在地" icon="ep:location" />
+            </template>
+            {{ user.areaName }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="注册 IP" icon="ep:position" />
+            </template>
+            {{ user.registerIp }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="生日" icon="fa:birthday-cake" />
+            </template>
+            {{ user.birthday ? formatDate(user.birthday) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="注册时间" icon="ep:calendar" />
+            </template>
+            {{ user.createTime ? formatDate(user.createTime) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="最后登录时间" icon="ep:calendar" />
+            </template>
+            {{ user.loginDate ? formatDate(user.loginDate) : '空' }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-col>
+    </el-row>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as UserApi from '@/api/member/user'
+import { DescriptionsItemLabel } from '@/components/Descriptions/index'
+
+const { user } = defineProps<{ user: UserApi.UserVO }>()
+</script>
+<style scoped lang="scss">
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 125 - 0
src/views/service/project/detail/UserBrokerageList.vue

@@ -0,0 +1,125 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="85px"
+    >
+      <el-form-item label="用户类型" prop="level">
+        <el-radio-group v-model="queryParams.level" @change="handleQuery">
+          <el-radio-button checked>全部</el-radio-button>
+          <el-radio-button label="1">一级推广人</el-radio-button>
+          <el-radio-button label="2">二级推广人</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="绑定时间" prop="bindUserTime">
+        <el-date-picker
+          v-model="queryParams.bindUserTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="用户编号" align="center" prop="id" min-width="80px" />
+      <el-table-column label="头像" align="center" prop="avatar" width="70px">
+        <template #default="scope">
+          <el-avatar :src="scope.row.avatar" />
+        </template>
+      </el-table-column>
+      <el-table-column label="昵称" align="center" prop="nickname" min-width="80px" />
+      <el-table-column label="等级" align="center" prop="level" min-width="80px">
+        <template #default="scope">
+          <el-tag v-if="scope.row.bindUserId === bindUserId">一级</el-tag>
+          <el-tag v-else>二级</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="绑定时间"
+        align="center"
+        prop="bindUserTime"
+        :formatter="dateFormatter"
+        width="170px"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+
+/** 推广人列表 */
+defineOptions({ name: 'UserBrokerageList' })
+
+const { bindUserId }: { bindUserId: number } = defineProps({
+  bindUserId: {
+    type: Number,
+    required: true
+  }
+}) //用户编号
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  bindUserId: null,
+  level: '',
+  bindUserTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.bindUserId = bindUserId
+    const data = await BrokerageUserApi.getBrokerageUserPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 190 - 0
src/views/service/project/detail/UserCouponList.vue

@@ -0,0 +1,190 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />搜索 </el-button>
+        <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <!-- Tab 选项:真正的内容在 Lab -->
+    <el-tabs v-model="activeTab" type="card" @tab-change="onTabChange">
+      <el-tab-pane
+        v-for="tab in statusTabs"
+        :key="tab.value"
+        :label="tab.label"
+        :name="tab.value"
+      />
+    </el-tabs>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="优惠劵" align="center" prop="name" />
+      <el-table-column label="优惠券类型" align="center" prop="discountType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="领取方式" align="center" prop="takeType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="领取时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column
+        label="使用时间"
+        align="center"
+        prop="useTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['promotion:coupon:delete']"
+            type="danger"
+            link
+            @click="handleDelete(scope.row.id)"
+          >
+            回收
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts" name="UserCouponList">
+import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon/coupon'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'UserCouponList' })
+
+const { userId }: { userId: number } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+}) //用户编号
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 字典表格数据
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  createTime: [],
+  status: undefined,
+  userIds: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+const activeTab = ref('all') // Tab 筛选
+const statusTabs = reactive([
+  {
+    label: '全部',
+    value: 'all'
+  }
+])
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  // 执行查询
+  try {
+    queryParams.userIds = userId
+    const data = await getCouponPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 二次确认
+    await message.confirm(
+      '回收将会收回会员领取的待使用的优惠券,已使用的将无法回收,确定要回收所选优惠券吗?'
+    )
+    // 发起删除
+    await deleteCoupon(id)
+    message.notifySuccess('回收成功')
+    // 重新加载列表
+    await getList()
+  } catch {}
+}
+
+/** tab 切换 */
+const onTabChange = (tabName) => {
+  queryParams.status = tabName === 'all' ? undefined : tabName
+  getList()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+  // 设置 statuses 过滤
+  for (const dict of getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_STATUS)) {
+    statusTabs.push({
+      label: dict.label,
+      value: dict.value as string
+    })
+  }
+})
+</script>

+ 158 - 0
src/views/service/project/detail/UserExperienceRecordList.vue

@@ -0,0 +1,158 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="业务类型" prop="bizType">
+        <el-select
+          v-model="queryParams.bizType"
+          placeholder="请选择业务类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="标题" prop="title">
+        <el-input
+          v-model="queryParams.title"
+          placeholder="请输入标题"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" width="150px" />
+      <el-table-column
+        label="获得时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="经验" align="center" prop="experience" width="150px">
+        <template #default="scope">
+          <el-tag v-if="scope.row.experience > 0" class="ml-2" type="success" effect="dark">
+            +{{ scope.row.experience }}
+          </el-tag>
+          <el-tag v-else class="ml-2" type="danger" effect="dark">
+            {{ scope.row.experience }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="总经验" align="center" prop="totalExperience" width="150px">
+        <template #default="scope">
+          <el-tag class="ml-2" effect="dark">
+            {{ scope.row.totalExperience }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="标题" align="center" prop="title" width="150px" />
+      <el-table-column label="描述" align="center" prop="description" />
+      <el-table-column label="业务编号" align="center" prop="bizId" width="150px" />
+      <el-table-column label="业务类型" align="center" prop="bizType" width="150px">
+        <!--   TODO 芋艿:此处应创建对应的字典 -->
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE" :value="scope.row.bizType" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as ExperienceRecordApi from '@/api/member/experience-record/index'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'UserExperienceRecordList' })
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: null,
+  bizId: null,
+  bizType: null,
+  title: null,
+  description: null,
+  experience: null,
+  totalExperience: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ExperienceRecordApi.getExperienceRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 96 - 0
src/views/service/project/detail/UserFavoriteList.vue

@@ -0,0 +1,96 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column key="id" align="center" label="商品编号" width="180" prop="id" />
+      <el-table-column label="商品图" min-width="80">
+        <template #default="{ row }">
+          <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+        </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 }"> {{ floatToFixed2(row.price) }}元</template>
+      </el-table-column>
+      <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="收藏时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="状态" min-width="80">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PRODUCT_SPU_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as FavoriteApi from '@/api/mall/product/favorite'
+import { floatToFixed2 } from '@/utils'
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  createTime: [],
+  userId: NaN
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await FavoriteApi.getFavoritePage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 279 - 0
src/views/service/project/detail/UserOrderList.vue

@@ -0,0 +1,279 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="订单状态" prop="status">
+        <el-select v-model="queryParams.status" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="支付方式" prop="payChannelCode">
+        <el-select
+          v-model="queryParams.payChannelCode"
+          class="!w-280px"
+          clearable
+          placeholder="全部"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-280px"
+          end-placeholder="自定义时间"
+          start-placeholder="自定义时间"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="订单来源" prop="terminal">
+        <el-select v-model="queryParams.terminal" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TERMINAL)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="订单类型" prop="type">
+        <el-select v-model="queryParams.type" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="配送方式" prop="deliveryType">
+        <el-select v-model="queryParams.deliveryType" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+        label="快递公司"
+        prop="logisticsId"
+      >
+        <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="item in deliveryExpressList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+        label="自提门店"
+        prop="pickUpStoreId"
+      >
+        <el-select
+          v-model="queryParams.pickUpStoreId"
+          class="!w-280px"
+          clearable
+          multiple
+          placeholder="全部"
+        >
+          <el-option
+            v-for="item in pickUpStoreList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+        label="核销码"
+        prop="pickUpVerifyCode"
+      >
+        <el-input
+          v-model="queryParams.pickUpVerifyCode"
+          class="!w-280px"
+          clearable
+          placeholder="请输入自提核销码"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="聚合搜索">
+        <el-input
+          v-show="true"
+          v-model="queryParams[queryType.queryParam]"
+          class="!w-280px"
+          clearable
+          placeholder="请输入"
+        >
+          <template #prepend>
+            <el-select
+              v-model="queryType.queryParam"
+              class="!w-110px"
+              clearable
+              placeholder="全部"
+              @change="inputChangeSelect"
+            >
+              <el-option
+                v-for="dict in dynamicSearchList"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-input>
+      </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-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <!-- 添加 row-key="id" 解决列数据中的 table#header 数据不刷新的问题  -->
+    <el-table v-loading="loading" :data="list" row-key="id">
+<!--      <OrderTableColumn :list="list" :pick-up-store-list="pickUpStoreList">-->
+<!--        <template #default="{ row }">-->
+<!--          <el-button link type="primary" @click="openDetail(row.id)">-->
+<!--            <Icon icon="ep:notification" />-->
+<!--            详情-->
+<!--          </el-button>-->
+<!--        </template>-->
+<!--      </OrderTableColumn>-->
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as OrderApi from '@/api/mall/trade/order/index'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import { FormInstance } from 'element-plus'
+// import { OrderTableColumn } from '@/views/mall/trade/order/components'
+import { DeliveryTypeEnum } from '@/utils/constants'
+
+const { push } = useRouter() // 路由跳转
+
+const { userId } = defineProps<{
+  userId: number
+}>()
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const pickUpStoreList = ref<PickUpStoreApi.DeliveryPickUpStoreVO[]>([]) // 自提门店精简列表
+const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 物流公司
+const queryFormRef = ref<FormInstance>() // 搜索的表单
+// 表单搜索
+const queryParams = ref({
+  pageNo: 1, // 页数
+  pageSize: 10, // 每页显示数量
+  userId: userId,
+  status: undefined, // 订单状态
+  payChannelCode: undefined, // 支付方式
+  createTime: undefined, // 创建时间
+  terminal: undefined, // 订单来源
+  type: undefined, // 订单类型
+  deliveryType: undefined, // 配送方式
+  logisticsId: undefined, // 快递公司
+  pickUpStoreId: undefined, // 自提门店
+  pickUpVerifyCode: undefined // 自提核销码
+})
+const queryType = reactive({ queryParam: '' }) // 订单搜索类型 queryParam
+
+// 订单聚合搜索 select 类型配置(动态搜索)
+const dynamicSearchList = ref([
+  { value: 'no', label: '订单号' },
+  { value: 'userNickname', label: '用户昵称' },
+  { value: 'userMobile', label: '用户电话' }
+])
+/**
+ * 聚合搜索切换查询对象时触发
+ * @param val
+ */
+const inputChangeSelect = (val: string) => {
+  dynamicSearchList.value
+    .filter((item) => item.value !== val)
+    ?.forEach((item1) => {
+      // 清除集合搜索无用属性
+      if (queryParams.value.hasOwnProperty(item1.value)) {
+        delete queryParams.value[item1.value]
+      }
+    })
+}
+
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  queryParams.value.pageNo = 1
+  await getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.value.userId = userId
+  handleQuery()
+}
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await OrderApi.getOrderPage(queryParams.value)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 查看订单详情 */
+const openDetail = (id: number) => {
+  push({ name: 'TradeOrderDetail', params: { id } })
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+  deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+})
+</script>

+ 152 - 0
src/views/service/project/detail/UserPointList.vue

@@ -0,0 +1,152 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="业务类型" prop="bizType">
+        <el-select
+          v-model="queryParams.bizType"
+          placeholder="请选择业务类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_POINT_BIZ_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="积分标题" prop="title">
+        <el-input
+          v-model="queryParams.title"
+          placeholder="请输入积分标题"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="获得时间" prop="createDate">
+        <el-date-picker
+          v-model="queryParams.createDate"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" width="180" />
+      <el-table-column
+        label="获得时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column label="获得积分" align="center" prop="point" width="100">
+        <template #default="scope">
+          <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+            +{{ scope.row.point }}
+          </el-tag>
+          <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="总积分" align="center" prop="totalPoint" width="100" />
+      <el-table-column label="标题" align="center" prop="title" />
+      <el-table-column label="描述" align="center" prop="description" />
+      <el-table-column label="业务编码" align="center" prop="bizId" />
+      <el-table-column label="业务类型" align="center" prop="bizType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.MEMBER_POINT_BIZ_TYPE" :value="scope.row.bizType" />
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as RecordApi from '@/api//member/point/record'
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  bizType: undefined,
+  title: null,
+  createDate: [],
+  userId: NaN
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await RecordApi.getRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 135 - 0
src/views/service/project/detail/UserSignList.vue

@@ -0,0 +1,135 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="签到用户" prop="nickname">
+        <el-input
+          v-model="queryParams.nickname"
+          placeholder="请输入签到用户"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="签到天数" prop="day">
+        <el-input
+          v-model="queryParams.day"
+          placeholder="请输入签到天数"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="签到时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column
+        label="签到天数"
+        align="center"
+        prop="day"
+        :formatter="(_, __, cellValue) => ['第', cellValue, '天'].join(' ')"
+      />
+      <el-table-column label="获得积分" align="center" prop="point" width="100">
+        <template #default="scope">
+          <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+            +{{ scope.row.point }}
+          </el-tag>
+          <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="签到时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as SignInRecordApi from '@/api/member/signin/record'
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: NaN,
+  nickname: null,
+  day: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SignInRecordApi.getSignInRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+const { userId } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+/** 初始化 **/
+onMounted(() => {
+  queryParams.userId = userId
+  getList()
+})
+</script>

+ 135 - 0
src/views/service/project/detail/index.vue

@@ -0,0 +1,135 @@
+<template>
+  <div v-loading="loading">
+    <el-row :gutter="10">
+      <!-- 左上角:基本信息 -->
+      <el-col :span="14" class="detail-info-item">
+        <UserBasicInfo :user="user">
+          <template #header>
+            <div class="card-header">
+              <CardTitle title="基本信息" />
+              <el-button type="primary" size="small" text @click="openForm('update')">
+                编辑
+              </el-button>
+            </div>
+          </template>
+        </UserBasicInfo>
+      </el-col>
+      <!-- 右上角:账户信息 -->
+      <el-col :span="10" class="detail-info-item">
+        <el-card shadow="never" class="h-full">
+          <template #header>
+            <CardTitle title="账户信息" />
+          </template>
+          <UserAccountInfo :user="user" />
+        </el-card>
+      </el-col>
+      <!-- 下边:账户明细 -->
+      <!-- TODO 芋艿:【订单管理】【售后管理】【收藏记录】-->
+      <el-card header="账户明细" style="width: 100%; margin-top: 20px" shadow="never">
+        <template #header>
+          <CardTitle title="账户明细" />
+        </template>
+        <el-tabs>
+          <el-tab-pane label="积分">
+            <UserPointList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="签到" lazy>
+            <UserSignList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="成长值" lazy>
+            <UserExperienceRecordList :user-id="id" />
+          </el-tab-pane>
+          <!-- TODO @jason:增加一个余额变化; -->
+          <el-tab-pane label="余额" lazy>余额(WIP)</el-tab-pane>
+          <el-tab-pane label="收货地址" lazy>
+            <UserAddressList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="订单管理" lazy>
+            <UserOrderList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="售后管理" lazy>售后管理(WIP)</el-tab-pane>
+          <el-tab-pane label="收藏记录" lazy>
+            <UserFavoriteList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="优惠劵" lazy>
+            <UserCouponList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="推广用户" lazy>
+            <UserBrokerageList :bind-user-id="id" />
+          </el-tab-pane>
+        </el-tabs>
+      </el-card>
+    </el-row>
+  </div>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <UserForm ref="formRef" @success="getUserData(id)" />
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import UserForm from '@/views/member/user/UserForm.vue'
+import UserAccountInfo from './UserAccountInfo.vue'
+import UserAddressList from './UserAddressList.vue'
+import UserBasicInfo from './UserBasicInfo.vue'
+import UserBrokerageList from './UserBrokerageList.vue'
+import UserCouponList from './UserCouponList.vue'
+import UserExperienceRecordList from './UserExperienceRecordList.vue'
+import UserOrderList from './UserOrderList.vue'
+import UserPointList from './UserPointList.vue'
+import UserSignList from './UserSignList.vue'
+import UserFavoriteList from './UserFavoriteList.vue'
+import { CardTitle } from '@/components/Card/index'
+import { ElMessage } from 'element-plus'
+
+defineOptions({ name: 'MemberDetail' })
+
+const loading = ref(true) // 加载中
+const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string) => {
+  formRef.value.open(type, id)
+}
+
+/** 获得用户 */
+const getUserData = async (id: number) => {
+  loading.value = true
+  try {
+    user.value = await UserApi.getUser(id)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 */
+const { currentRoute } = useRouter() // 路由
+const { delView } = useTagsViewStore() // 视图操作
+const route = useRoute()
+const id = Number(route.params.id)
+onMounted(() => {
+  if (!id) {
+    ElMessage.warning('参数错误,会员编号不能为空!')
+    delView(unref(currentRoute))
+    return
+  }
+  getUserData(id)
+})
+</script>
+<style scoped lang="css">
+.detail-info-item:first-child {
+  padding-left: 0 !important;
+}
+
+/* first-child 不生效有没有大佬给看下q.q */
+.detail-info-item:nth-child(2) {
+  padding-right: 0 !important;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 284 - 0
src/views/service/project/index.vue

@@ -0,0 +1,284 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="服务名称" prop="title">
+        <el-input
+          v-model="queryParams.title"
+          class="!w-240px"
+          clearable
+          placeholder="请输入服务名称"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="请选择状态">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="注册时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </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
+          v-hasPermi="['system:role:create']"
+          plain
+          type="primary"
+          @click="openForm('create')"
+        >
+          <Icon class="mr-5px" icon="ep:plus"/>
+          新增
+        </el-button>
+
+        <el-button v-hasPermi="['promotion:coupon:send']" @click="openCoupon">发送优惠券</el-button>
+      </el-form-item>
+
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      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="id"/>
+      <el-table-column align="center" label="封面图" prop="cover">
+        <template #default="scope">
+          <img :src="scope.row.cover" style="width: 40px"/>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="服务名称" prop="title"/>
+      <el-table-column align="center" label="价格" prop="price">
+        <template #default="scope">¥ {{ roundFormatter(scope.row.price / 100) }}</template>
+      </el-table-column>
+      <el-table-column align="center" label="原价" prop="initPrice" >
+        <template #default="scope">¥ {{ roundFormatter(scope.row.initPrice / 100) }}</template>
+      </el-table-column>
+      <el-table-column align="center" label="物料费" prop="materialPrice">
+        <template #default="scope">¥ {{ roundFormatter(scope.row.materialPrice / 100) }}</template>
+      </el-table-column>
+      <el-table-column align="center" label="服务时长" prop="timeLong">
+        <template #default="scope">{{ scope.row.timeLong }} 分钟</template>
+      </el-table-column>
+      <el-table-column align="center" label="排序" prop="sort"/>
+      <el-table-column align="center" label="状态" prop="status">
+        <template #default="scope">
+          <el-switch
+            v-model="scope.row.status"
+            :active-value="0"
+            :inactive-value="1"
+            @change="handleStatusChange(scope.row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
+        width="180px"
+      />
+      <el-table-column
+        :show-overflow-tooltip="false"
+        align="center"
+        fixed="right"
+        label="操作"
+        width="100px"
+      >
+        <template #default="scope">
+          <div class="flex items-center justify-center">
+            <el-button link type="primary" @click="openDetail(scope.row.id)">详情</el-button>
+            <el-dropdown
+              v-hasPermi="[
+                'member:user:update',
+                'member:user:update-level',
+                'member:user:update-point',
+                'member:user:update-balance'
+              ]"
+              @command="(command) => handleCommand(command, scope.row)"
+            >
+              <el-button link type="primary">
+                <Icon icon="ep:d-arrow-right"/>
+                更多
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item
+                    v-if="checkPermi(['member:user:update'])"
+                    command="handleUpdate"
+                  >
+                    编辑
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    v-if="checkPermi(['member:user:update-level'])"
+                    command="handleUpdateLevel"
+                  >
+                    修改等级
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    v-if="checkPermi(['member:user:update-point'])"
+                    command="handleUpdatePoint"
+                  >
+                    修改积分
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    v-if="checkPermi(['member:user:update-balance'])"
+                    command="handleUpdateBlance"
+                  >
+                    修改余额(WIP)
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <UserForm ref="formRef" @success="getList"/>
+  <!-- 修改用户等级弹窗 -->
+  <UserLevelUpdateForm ref="updateLevelFormRef" @success="getList"/>
+  <!-- 修改用户积分弹窗 -->
+  <UserPointUpdateForm ref="updatePointFormRef" @success="getList"/>
+</template>
+<script lang="ts" setup>
+import {dateFormatter} from '@/utils/formatTime'
+import * as ProjectApi from '@/api/service/project'
+import {DICT_TYPE, getIntDictOptions} from '@/utils/dict'
+import UserForm from './UserForm.vue'
+import UserLevelUpdateForm from './UserLevelUpdateForm.vue'
+import UserPointUpdateForm from './UserPointUpdateForm.vue'
+import {checkPermi} from '@/utils/permission'
+
+defineOptions({name: 'MemberUser'})
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  title: null,
+  status: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const updateLevelFormRef = ref() // 修改会员等级表单
+const updatePointFormRef = ref() // 修改会员积分表单
+const selectedIds = ref<number[]>([]) // 表格的选中 ID 数组
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProjectApi.getProjectPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 打开会员详情 */
+const {push} = useRouter()
+const openDetail = (id: number) => {
+  push({name: 'MemberUserDetail', params: {id}})
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 表格选中事件 */
+const handleSelectionChange = (rows: ProjectApi.Project[]) => {
+  selectedIds.value = rows.map((row) => row.id)
+}
+
+/** 操作分发 */
+const handleCommand = (command: string, row: ProjectApi.Project) => {
+  switch (command) {
+    case 'handleUpdate':
+      openForm('update', row.id)
+      break
+    case 'handleUpdateLevel':
+      updateLevelFormRef.value.open(row.id)
+      break
+    case 'handleUpdatePoint':
+      updatePointFormRef.value.open(row.id)
+      break
+    case 'handleUpdateBlance':
+      // todo @jason:增加一个【修改余额】
+      break
+    default:
+      break
+  }
+}
+
+// 保留两位小数
+const roundFormatter = (cellValue) => {
+  return cellValue.toFixed(2); // 四舍五入保留两位小数
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>