Ver código fonte

Develop 新規作成

libin 1 ano atrás
pai
commit
c99165ebec
100 arquivos alterados com 14195 adições e 402 exclusões
  1. 1 1
      .env
  2. 2 2
      .vscode/launch.json
  3. 2 2
      index.html
  4. 4 1
      package.json
  5. 43 0
      src/api/guide/schedule/index.ts
  6. 121 0
      src/api/guide/sights/index.ts
  7. 50 0
      src/api/guide/sightscategory/index.ts
  8. 5 0
      src/api/guide/statistics/common.ts
  9. 53 0
      src/api/guide/statistics/guide.ts
  10. 123 0
      src/api/guide/statistics/member.ts
  11. 12 0
      src/api/guide/statistics/pay.ts
  12. 53 0
      src/api/guide/statistics/sights.ts
  13. 119 0
      src/api/guide/statistics/trade.ts
  14. 53 0
      src/api/guide/statistics/trip.ts
  15. 80 0
      src/api/guide/trade/afterSale/index.ts
  16. 162 0
      src/api/guide/trade/order/index.ts
  17. 100 0
      src/api/guide/trip/index.ts
  18. 92 0
      src/api/guide/users/index.ts
  19. 37 1
      src/api/system/user/profile.ts
  20. BIN
      src/assets/imgs/image-panda-removebg-preview.png
  21. 1 1
      src/components/AppLinkInput/data.ts
  22. 3 0
      src/components/QuillEditor/index.ts
  23. 255 0
      src/components/QuillEditor/src/QEditor.vue
  24. 5 6
      src/components/ShortcutDateRangePicker/index.vue
  25. 1 1
      src/layout/components/Footer/src/Footer.vue
  26. 4 3
      src/layout/components/Logo/src/Logo.vue
  27. 3 3
      src/layout/components/UserInfo/src/UserInfo.vue
  28. 2 2
      src/layout/components/UserInfo/src/components/LockDialog.vue
  29. 688 9
      src/locales/en.ts
  30. 1136 0
      src/locales/ja.ts
  31. 688 3
      src/locales/zh-CN.ts
  32. 24 3
      src/router/modules/remaining.ts
  33. 5 1
      src/store/modules/locale.ts
  34. 1 0
      src/types/components.d.ts
  35. 5 1
      src/utils/dict.ts
  36. 1 0
      src/utils/tree.ts
  37. 384 0
      src/views/Home BK/Index.vue
  38. 0 0
      src/views/Home BK/Index2.vue
  39. 0 0
      src/views/Home BK/echarts-data.ts
  40. 55 0
      src/views/Home BK/types.ts
  41. 105 341
      src/views/Home/Index.vue
  42. 42 0
      src/views/Home/components/ComparisonCard.vue
  43. 91 0
      src/views/Home/components/MemberStatisticsCard.vue
  44. 104 0
      src/views/Home/components/OperationDataCard.vue
  45. 84 0
      src/views/Home/components/ShortcutCard.vue
  46. 208 0
      src/views/Home/components/TradeTrendCard.vue
  47. 9 9
      src/views/Login/Login.vue
  48. 10 10
      src/views/Login/components/LoginForm.vue
  49. 2 2
      src/views/Login/components/MobileForm.vue
  50. 276 0
      src/views/guide/profile/components/BasicInfo.vue
  51. 256 0
      src/views/guide/profile/components/Calendar.vue
  52. 188 0
      src/views/guide/profile/components/SelfCommentInfo.vue
  53. 76 0
      src/views/guide/profile/index.vue
  54. 132 0
      src/views/guide/schedule/ScheduleForm.vue
  55. 249 0
      src/views/guide/schedule/index.vue
  56. 265 0
      src/views/guide/sights/SightsDetail.vue
  57. 156 0
      src/views/guide/sights/SightsForm.vue
  58. 42 0
      src/views/guide/sights/components/SightsCategoryDisplay.vue
  59. 68 0
      src/views/guide/sights/components/SightsCategorySelect.vue
  60. 160 0
      src/views/guide/sights/components/SightsCommentForm.vue
  61. 91 0
      src/views/guide/sights/components/SightsCommentForm2.vue
  62. 153 0
      src/views/guide/sights/components/SightsCommentList.vue
  63. 191 0
      src/views/guide/sights/components/SightsI18nExtensionForm.vue
  64. 157 0
      src/views/guide/sights/components/SightsI18nExtensionForm2.vue
  65. 137 0
      src/views/guide/sights/components/SightsI18nExtensionList.vue
  66. 242 0
      src/views/guide/sights/index.vue
  67. 158 0
      src/views/guide/sightscategory/SightsCategoryForm.vue
  68. 254 0
      src/views/guide/sightscategory/index.vue
  69. 101 0
      src/views/guide/statistics/guide/components/GuideRank.vue
  70. 294 0
      src/views/guide/statistics/guide/components/GuideSummary.vue
  71. 16 0
      src/views/guide/statistics/guide/index.vue
  72. 121 0
      src/views/guide/statistics/member/components/MemberFunnelCard.vue
  73. 69 0
      src/views/guide/statistics/member/components/MemberTerminalCard.vue
  74. 312 0
      src/views/guide/statistics/member/index.vue
  75. 107 0
      src/views/guide/statistics/sights/components/SightsRank.vue
  76. 294 0
      src/views/guide/statistics/sights/components/SightsSummary.vue
  77. 16 0
      src/views/guide/statistics/sights/index.vue
  78. 36 0
      src/views/guide/statistics/trade/components/TradeStatisticValue.vue
  79. 353 0
      src/views/guide/statistics/trade/index.vue
  80. 107 0
      src/views/guide/statistics/trip/components/TripRank.vue
  81. 294 0
      src/views/guide/statistics/trip/components/TripSummary.vue
  82. 16 0
      src/views/guide/statistics/trip/index.vue
  83. 325 0
      src/views/guide/trade/afterSale/detail/index.vue
  84. 70 0
      src/views/guide/trade/afterSale/form/AfterSaleDisagreeForm.vue
  85. 267 0
      src/views/guide/trade/afterSale/index.vue
  86. 171 0
      src/views/guide/trade/brokerage/record/index.vue
  87. 152 0
      src/views/guide/trade/brokerage/user/BrokerageOrderListDialog.vue
  88. 137 0
      src/views/guide/trade/brokerage/user/BrokerageUserListDialog.vue
  89. 127 0
      src/views/guide/trade/brokerage/user/UpdateBindUserForm.vue
  90. 307 0
      src/views/guide/trade/brokerage/user/index.vue
  91. 73 0
      src/views/guide/trade/brokerage/withdraw/BrokerageWithdrawRejectForm.vue
  92. 268 0
      src/views/guide/trade/brokerage/withdraw/index.vue
  93. 291 0
      src/views/guide/trade/config/index.vue
  94. 126 0
      src/views/guide/trade/delivery/express/ExpressForm.vue
  95. 189 0
      src/views/guide/trade/delivery/express/index.vue
  96. 321 0
      src/views/guide/trade/delivery/expressTemplate/ExpressTemplateForm.vue
  97. 165 0
      src/views/guide/trade/delivery/expressTemplate/index.vue
  98. 328 0
      src/views/guide/trade/delivery/pickUpOrder/index.vue
  99. 273 0
      src/views/guide/trade/delivery/pickUpStore/PickUpStoreForm.vue
  100. 190 0
      src/views/guide/trade/delivery/pickUpStore/index.vue

+ 1 - 1
.env

@@ -1,5 +1,5 @@
 # 标题
-VITE_APP_TITLE=芋道管理系统
+VITE_APP_TITLE=旅行者とツアーガイドのマッチング管理システム
 
 # 项目本地运行端口号
 VITE_PORT=80

+ 2 - 2
.vscode/launch.json

@@ -5,9 +5,9 @@
   "version": "0.2.0",
   "configurations": [
     {
-      "type": "msedge",
+      "type": "chrome",
       "request": "launch",
-      "name": "Launch Edge against localhost",
+      "name": "Launch chrome against localhost",
       "url": "http://localhost",
       "webRoot": "${workspaceFolder}/src",
       "sourceMaps": true

+ 2 - 2
index.html

@@ -7,11 +7,11 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <meta
       name="keywords"
-      content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
+      content="旅行者とツアーガイドのマッチング管理システム 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
     />
     <meta
       name="description"
-      content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
+      content="旅行者とツアーガイドのマッチング管理システム 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
     />
     <title>%VITE_APP_TITLE%</title>
   </head>

+ 4 - 1
package.json

@@ -30,6 +30,7 @@
     "@form-create/element-ui": "^3.1.24",
     "@iconify/iconify": "^3.1.1",
     "@videojs-player/vue": "^1.0.0",
+    "@vueup/vue-quill": "^1.2.0",
     "@vueuse/core": "^10.9.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.10",
@@ -41,6 +42,7 @@
     "camunda-bpmn-moddle": "^7.0.1",
     "cropperjs": "^1.6.1",
     "crypto-js": "^4.2.0",
+    "date-fns": "^3.6.0",
     "dayjs": "^1.11.10",
     "diagram-js": "^12.8.0",
     "driver.js": "^1.3.1",
@@ -62,6 +64,7 @@
     "url": "^0.11.3",
     "video.js": "^7.21.5",
     "vue": "3.4.21",
+    "vue-cal": "^4.8.1",
     "vue-dompurify-html": "^4.1.4",
     "vue-i18n": "9.10.2",
     "vue-router": "^4.3.0",
@@ -83,8 +86,8 @@
     "@types/qs": "^6.9.12",
     "@typescript-eslint/eslint-plugin": "^7.1.0",
     "@typescript-eslint/parser": "^7.1.0",
-    "@unocss/transformer-variant-group": "^0.58.5",
     "@unocss/eslint-config": "^0.57.4",
+    "@unocss/transformer-variant-group": "^0.58.5",
     "@vitejs/plugin-legacy": "^5.3.1",
     "@vitejs/plugin-vue": "^5.0.4",
     "@vitejs/plugin-vue-jsx": "^3.1.0",

+ 43 - 0
src/api/guide/schedule/index.ts

@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+// 预约旅程日程管理情报 VO
+export interface ScheduleVO {
+  id: number // 旅程日程编号
+  guideId: number // 导游编号
+  bookingDate: Date // 预约日期
+  bookingAvailability: boolean // 是否可以被预约
+  booked: boolean // 已经被预约否?
+}
+
+// 预约旅程日程管理情报 API
+export const ScheduleApi = {
+  // 查询预约旅程日程管理情报分页
+  getSchedulePage: async (id: number) => {
+    return await request.get({ url: `/guide/schedule/list?guideId=` + id })
+  },
+
+  // 查询预约旅程日程管理情报详情
+  getSchedule: async (id: number) => {
+    return await request.get({ url: `/guide/schedule/get?id=` + id })
+  },
+
+  // 新增预约旅程日程管理情报
+  createSchedule: async (data: ScheduleVO) => {
+    return await request.post({ url: `/guide/schedule/create`, data })
+  },
+
+  // 修改预约旅程日程管理情报
+  updateSchedule: async (data: ScheduleVO) => {
+    return await request.put({ url: `/guide/schedule/update`, data })
+  },
+
+  // 删除预约旅程日程管理情报
+  deleteSchedule: async (id: number) => {
+    return await request.delete({ url: `/guide/schedule/delete?id=` + id })
+  },
+
+  // 导出预约旅程日程管理情报 Excel
+  exportSchedule: async (params) => {
+    return await request.download({ url: `/guide/schedule/export-excel`, params })
+  }
+}

+ 121 - 0
src/api/guide/sights/index.ts

@@ -0,0 +1,121 @@
+import request from '@/config/axios'
+
+export interface SightsI18nExtensionVO {
+  id: number
+  sightsId: number
+  languageType: number
+  title: string
+  subtitle: string
+  overview: string
+  description: string
+  address: string
+  keywords: string
+  memo: string
+}
+// 观光景点基本信息 VO
+export interface SightsVO {
+  id: number // 编号
+  categoryIds: string // 景点分类数组,以逗号分隔
+  picUrl: string // 景点封面图
+  sliderPicUrls: string // 景点轮播图地址数组,以逗号分隔,最多10
+  url: string // 官网URL
+  tel: string // 电话号码
+  opentime: string // 开放时间
+  durationSightseeing: number // 大致游玩所需时间
+  geographicalIds: string // 地理信息表ID
+  sort: number // 排序
+  status: number // 状态: 1 上架(开启) 0 下架(禁用) -1 回收
+  sightsI18nExtensionDOList: SightsI18nExtensionVO[]
+}
+
+// 观光景点基本信息 API
+export const SightsApi = {
+  // 查询观光景点基本信息分页
+  getSightsPage: async (params: any) => {
+    return await request.get({ url: `/guide/sights/page`, params })
+  },
+
+  // 查询观光景点基本信息List(不包含在trip-sights中的列表)
+  getSightsList: async (params: any) => {
+    return await request.get({ url: `/guide/sights/list`, params })
+  },
+  // 查询观光景点基本信息详情
+  getSights: async (id: number) => {
+    return await request.get({ url: `/guide/sights/get?id=` + id })
+  },
+
+  // 新增观光景点基本信息
+  createSights: async (data: SightsVO) => {
+    return await request.post({ url: `/guide/sights/create`, data })
+  },
+
+  // 修改观光景点基本信息
+  updateSights: async (data: SightsVO) => {
+    return await request.put({ url: `/guide/sights/update`, data })
+  },
+
+  // 删除观光景点基本信息
+  deleteSights: async (id: number) => {
+    return await request.delete({ url: `/guide/sights/delete?id=` + id })
+  },
+
+  // 导出观光景点基本信息 Excel
+  exportSights: async (params) => {
+    return await request.download({ url: `/guide/sights/export-excel`, params })
+  },
+
+   // 查询观光景点数量
+   getSightsCount: async () => {
+    return await request.get({ url: `/guide/sights/get-count` })
+  },
+// ==================== 子表(观光景点评论) ====================
+
+  // 获得观光景点评论分页
+  getSightsCommentPage: async (params) => {
+    return await request.get({ url: `/guide/sights/sights-comment/page`, params })
+  },
+  // 新增观光景点评论
+  createSightsComment: async (data) => {
+    return await request.post({ url: `/guide/sights/sights-comment/create`, data })
+  },
+
+  // 修改观光景点评论
+  updateSightsComment: async (data) => {
+    return await request.put({ url: `/guide/sights/sights-comment/update`, data })
+  },
+
+  // 删除观光景点评论
+  deleteSightsComment: async (id: number) => {
+    return await request.delete({ url: `/guide/sights/sights-comment/delete?id=` + id })
+  },
+
+  // 获得观光景点评论
+  getSightsComment: async (id: number) => {
+    return await request.get({ url: `/guide/sights/sights-comment/get?id=` + id })
+  },
+// ==================== 子表(观光景点对语言扩充信息) ====================
+
+  // 获得观光景点对语言扩充信息分页
+  getSightsI18nExtensionPage: async (params) => {
+    return await request.get({ url: `/guide/sights/sights-i18n-extension/page`, params })
+  },
+  // 新增观光景点对语言扩充信息
+  createSightsI18nExtension: async (data) => {
+    return await request.post({ url: `/guide/sights/sights-i18n-extension/create`, data })
+  },
+
+  // 修改观光景点对语言扩充信息
+  updateSightsI18nExtension: async (data) => {
+    return await request.put({ url: `/guide/sights/sights-i18n-extension/update`, data })
+  },
+
+  // 删除观光景点对语言扩充信息
+  deleteSightsI18nExtension: async (id: number) => {
+    return await request.delete({ url: `/guide/sights/sights-i18n-extension/delete?id=` + id })
+  },
+
+  // 获得观光景点对语言扩充信息
+  getSightsI18nExtension: async (id: number) => {
+    return await request.get({ url: `/guide/sights/sights-i18n-extension/get?id=` + id })
+  }
+}

+ 50 - 0
src/api/guide/sightscategory/index.ts

@@ -0,0 +1,50 @@
+import request from '@/config/axios'
+
+// 观光景点分类 VO
+export interface SightsCategoryVO {
+  id: number // 分类编号
+  parentId: number // 父分类编号
+  nameZh: string // 分类中文名称
+  nameJa: string // 分类日文名称
+  nameEn: string // 分类英文名称
+  nameOther: string // 分类XX名称
+  picUrl: string // 移动端分类图
+  sort: number // 分类排序
+  status: number // 开启状态
+}
+
+// 观光景点分类 API
+export const SightsCategoryApi = {
+  // 查询观光景点分类列表
+  getSightsCategoryList: async (params) => {
+    return await request.get({ url: `/guide/sights-category/list`, params })
+  },
+  // 查询观光景点基本信息详情(from ids)
+  getSightCategorys: async (ids: any) => {
+    return await request.get({ url: `/guide/sights-category/categorys?ids=` + ids })
+  },
+  // 查询观光景点分类详情
+  getSightsCategory: async (id: number) => {
+    return await request.get({ url: `/guide/sights-category/get?id=` + id })
+  },
+
+  // 新增观光景点分类
+  createSightsCategory: async (data: SightsCategoryVO) => {
+    return await request.post({ url: `/guide/sights-category/create`, data })
+  },
+
+  // 修改观光景点分类
+  updateSightsCategory: async (data: SightsCategoryVO) => {
+    return await request.put({ url: `/guide/sights-category/update`, data })
+  },
+
+  // 删除观光景点分类
+  deleteSightsCategory: async (id: number) => {
+    return await request.delete({ url: `/guide/sights-category/delete?id=` + id })
+  },
+
+  // 导出观光景点分类 Excel
+  exportSightsCategory: async (params) => {
+    return await request.download({ url: `/guide/sights-category/export-excel`, params })
+  }
+}

+ 5 - 0
src/api/guide/statistics/common.ts

@@ -0,0 +1,5 @@
+/** 数据对照 Response VO */
+export interface GuideDataComparisonRespVO<T> {
+  value: T
+  reference: T
+}

+ 53 - 0
src/api/guide/statistics/guide.ts

@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+
+export interface GuideStatisticsVO {
+  id: number
+  day: string
+  time: Date
+  guideId: number
+  nickName: string
+  picUrl: string
+  browseCount: number
+  browseUserCount: number
+  favoriteCount: number
+  cartCount: number
+  orderCount: number
+  orderPayCount: number
+  orderPayPrice: number
+  afterSaleCount: number
+  afterSaleRefundPrice: number
+  browseConvertPercent: number
+}
+
+// 商品统计 API
+export const GuideStatisticsApi = {
+  // 获得商品统计分析
+  getGuideStatisticsAnalyse: (params: any) => {
+    return request.get<GuideDataComparisonRespVO<GuideStatisticsVO>>({
+      url: '/guide/statistics/guide/analyse',
+      params
+    })
+  },
+  // 获得商品状况明细
+  getGuideStatisticsList: (params: any) => {
+    return request.get<GuideStatisticsVO[]>({
+      url: '/guide/statistics/guide/list',
+      params
+    })
+  },
+  // 导出获得商品状况明细 Excel
+  exportGuideStatisticsExcel: (params: any) => {
+    return request.download({
+      url: '/guide/statistics/guide/export-excel',
+      params
+    })
+  },
+  // 获得商品排行榜分页
+  getGuideStatisticsRankPage: async (params: any) => {
+    return await request.get({
+      url: `/guide/statistics/guide/rank-page`,
+      params
+    })
+  }
+}

+ 123 - 0
src/api/guide/statistics/member.ts

@@ -0,0 +1,123 @@
+import request from '@/config/axios'
+import dayjs from 'dayjs'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+import { formatDate } from '@/utils/formatTime'
+
+/** 会员分析 Request VO */
+export interface GuideMemberAnalyseReqVO {
+  times: dayjs.ConfigType[]
+}
+
+/** 会员分析 Response VO */
+export interface GuideMemberAnalyseRespVO {
+  visitUserCount: number
+  orderUserCount: number
+  payUserCount: number
+  atv: number
+  comparison: GuideDataComparisonRespVO<GuideMemberAnalyseComparisonRespVO>
+}
+
+/** 会员分析对照数据 Response VO */
+export interface GuideMemberAnalyseComparisonRespVO {
+  registerUserCount: number
+  visitUserCount: number
+  rechargeUserCount: number
+}
+
+/** 会员地区统计 Response VO */
+export interface GuideMemberAreaStatisticsRespVO {
+  areaId: number
+  areaName: string
+  userCount: number
+  orderCreateUserCount: number
+  orderPayUserCount: number
+  orderPayPrice: number
+}
+
+/** 会员性别统计 Response VO */
+export interface GuideMemberSexStatisticsRespVO {
+  sex: number
+  userCount: number
+}
+
+/** 会员统计 Response VO */
+export interface GuideMemberSummaryRespVO {
+  userCount: number
+  rechargeUserCount: number
+  rechargePrice: number
+  expensePrice: number
+}
+
+/** 会员终端统计 Response VO */
+export interface GuideMemberTerminalStatisticsRespVO {
+  terminal: number
+  userCount: number
+}
+
+/** 会员数量统计 Response VO */
+export interface GuideMemberCountRespVO {
+  /** 用户访问量 */
+  visitUserCount: string
+  /** 注册用户数量 */
+  registerUserCount: number
+}
+
+/** 会员注册数量 Response VO */
+export interface GuideMemberRegisterCountRespVO {
+  date: string
+  count: number
+}
+
+// 查询会员统计
+export const getMemberSummary = () => {
+  return request.get<GuideMemberSummaryRespVO>({
+    url: '/guide/statistics/member/summary'
+  })
+}
+
+// 查询会员分析数据
+export const getMemberAnalyse = (params: GuideMemberAnalyseReqVO) => {
+  return request.get<GuideMemberAnalyseRespVO>({
+    url: '/guide/statistics/member/analyse',
+    params: { times: [formatDate(params.times[0]), formatDate(params.times[1])] }
+  })
+}
+
+// 按照省份,查询会员统计列表
+export const getMemberAreaStatisticsList = () => {
+  return request.get<GuideMemberAreaStatisticsRespVO[]>({
+    url: '/guide/statistics/member/area-statistics-list'
+  })
+}
+
+// 按照性别,查询会员统计列表
+export const getMemberSexStatisticsList = () => {
+  return request.get<GuideMemberSexStatisticsRespVO[]>({
+    url: '/guide/statistics/member/sex-statistics-list'
+  })
+}
+
+// 按照终端,查询会员统计列表
+export const getMemberTerminalStatisticsList = () => {
+  return request.get<GuideMemberTerminalStatisticsRespVO[]>({
+    url: '/guide/statistics/member/terminal-statistics-list'
+  })
+}
+
+// 获得用户数量量对照
+export const getUserCountComparison = () => {
+  return request.get<GuideDataComparisonRespVO<GuideMemberCountRespVO>>({
+    url: '/guide/statistics/member/user-count-comparison'
+  })
+}
+
+// 获得会员注册数量列表
+export const getMemberRegisterCountList = (
+  beginTime: dayjs.ConfigType,
+  endTime: dayjs.ConfigType
+) => {
+  return request.get<GuideMemberRegisterCountRespVO[]>({
+    url: '/guide/statistics/member/register-count-list',
+    params: { times: [formatDate(beginTime), formatDate(endTime)] }
+  })
+}

+ 12 - 0
src/api/guide/statistics/pay.ts

@@ -0,0 +1,12 @@
+import request from '@/config/axios'
+
+/** 支付统计 */
+export interface GuidePaySummaryRespVO {
+  /** 充值金额,单位分 */
+  rechargePrice: number
+}
+
+/** 获取钱包充值金额 */
+export const getWalletRechargePrice = async () => {
+  return await request.get<GuidePaySummaryRespVO>({ url: `/guide/statistics/pay/summary` })
+}

+ 53 - 0
src/api/guide/statistics/sights.ts

@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+
+export interface SightsStatisticsVO {
+  id: number
+  day: string
+  time: Date
+  sightsId: number
+  sightsTitle: []
+  picUrl: string
+  browseCount: number
+  browseUserCount: number
+  favoriteCount: number
+  cartCount: number
+  orderCount: number
+  orderPayCount: number
+  orderPayPrice: number
+  afterSaleCount: number
+  afterSaleRefundPrice: number
+  browseConvertPercent: number
+}
+
+// 商品统计 API
+export const SightsStatisticsApi = {
+  // 获得商品统计分析
+  getSightsStatisticsAnalyse: (params: any) => {
+    return request.get<GuideDataComparisonRespVO<SightsStatisticsVO>>({
+      url: '/guide/statistics/sights/analyse',
+      params
+    })
+  },
+  // 获得商品状况明细
+  getSightsStatisticsList: (params: any) => {
+    return request.get<SightsStatisticsVO[]>({
+      url: '/guide/statistics/sights/list',
+      params
+    })
+  },
+  // 导出获得商品状况明细 Excel
+  exportSightsStatisticsExcel: (params: any) => {
+    return request.download({
+      url: '/guide/statistics/sights/export-excel',
+      params
+    })
+  },
+  // 获得商品排行榜分页
+  getSightsStatisticsRankPage: async (params: any) => {
+    return await request.get({
+      url: `/guide/statistics/sights/rank-page`,
+      params
+    })
+  }
+}

+ 119 - 0
src/api/guide/statistics/trade.ts

@@ -0,0 +1,119 @@
+import request from '@/config/axios'
+import dayjs from 'dayjs'
+import { formatDate } from '@/utils/formatTime'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+
+/** 交易统计 Response VO */
+export interface GuideTradeSummaryRespVO {
+  yesterdayOrderCount: number
+  monthOrderCount: number
+  yesterdayPayPrice: number
+  monthPayPrice: number
+}
+
+/** 交易状况 Request VO */
+export interface GuideTradeTrendReqVO {
+  times: [dayjs.ConfigType, dayjs.ConfigType]
+}
+
+/** 交易状况统计 Response VO */
+export interface GuideTradeTrendSummaryRespVO {
+  time: string
+  turnoverPrice: number
+  orderPayPrice: number
+  rechargePrice: number
+  expensePrice: number
+  walletPayPrice: number
+  brokerageSettlementPrice: number
+  afterSaleRefundPrice: number
+}
+
+/** 交易订单数量 Response VO */
+export interface GuideTradeOrderCountRespVO {
+  /** 待发货 */
+  undelivered?: number
+  /** 待核销 */
+  pickUp?: number
+  /** 退款中 */
+  afterSaleApply?: number
+  /** 提现待审核 */
+  auditingWithdraw?: number
+}
+
+/** 交易订单统计 Response VO */
+export interface GuideTradeOrderSummaryRespVO {
+  /** 支付订单商品数 */
+  orderPayCount?: number
+  /** 总支付金额,单位:分 */
+  orderPayPrice?: number
+}
+
+/** 订单量趋势统计 Response VO */
+export interface GuideTradeOrderTrendRespVO {
+  /** 日期 */
+  date: string
+  /** 订单数量 */
+  orderPayCount: number
+  /** 订单支付金额 */
+  orderPayPrice: number
+}
+
+// 查询交易统计
+export const getTradeStatisticsSummary = () => {
+  return request.get<GuideDataComparisonRespVO<GuideTradeSummaryRespVO>>({
+    url: '/guide/statistics/trade/summary'
+  })
+}
+
+// 获得交易状况统计
+export const getTradeStatisticsAnalyse = (params: GuideTradeTrendReqVO) => {
+  return request.get<GuideDataComparisonRespVO<GuideTradeTrendSummaryRespVO>>({
+    url: '/guide/statistics/trade/analyse',
+    params: formatDateParam(params)
+  })
+}
+
+// 获得交易状况明细
+export const getTradeStatisticsList = (params: GuideTradeTrendReqVO) => {
+  return request.get<GuideTradeTrendSummaryRespVO[]>({
+    url: '/guide/statistics/trade/list',
+    params: formatDateParam(params)
+  })
+}
+
+// 导出交易状况明细
+export const exportTradeStatisticsExcel = (params: GuideTradeTrendReqVO) => {
+  return request.download({
+    url: '/guide/statistics/trade/export-excel',
+    params: formatDateParam(params)
+  })
+}
+
+// 获得交易订单数量
+export const getOrderCount = async () => {
+  return await request.get<GuideTradeOrderCountRespVO>({ url: `/guide/statistics/trade/order-count` })
+}
+
+// 获得交易订单数量对照
+export const getOrderComparison = async () => {
+  return await request.get<GuideDataComparisonRespVO<GuideTradeOrderSummaryRespVO>>({
+    url: `/guide/statistics/trade/order-comparison`
+  })
+}
+
+// 获得订单量趋势统计
+export const getOrderCountTrendComparison = (
+  type: number,
+  beginTime: dayjs.ConfigType,
+  endTime: dayjs.ConfigType
+) => {
+  return request.get<GuideDataComparisonRespVO<GuideTradeOrderTrendRespVO>[]>({
+    url: '/guide/statistics/trade/order-count-trend',
+    params: { type, beginTime: formatDate(beginTime), endTime: formatDate(endTime) }
+  })
+}
+
+/** 时间参数需要格式化, 确保接口能识别 */
+const formatDateParam = (params: GuideTradeTrendReqVO) => {
+  return { times: [formatDate(params.times[0]), formatDate(params.times[1])] } as GuideTradeTrendReqVO
+}

+ 53 - 0
src/api/guide/statistics/trip.ts

@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+
+export interface TripStatisticsVO {
+  id: number
+  day: string
+  time: Date
+  guideId: number
+  nickName: string
+  picUrl: string
+  browseCount: number
+  browseUserCount: number
+  favoriteCount: number
+  cartCount: number
+  orderCount: number
+  orderPayCount: number
+  orderPayPrice: number
+  afterSaleCount: number
+  afterSaleRefundPrice: number
+  browseConvertPercent: number
+}
+
+// 商品统计 API
+export const TripStatisticsApi = {
+  // 获得商品统计分析
+  getTripStatisticsAnalyse: (params: any) => {
+    return request.get<GuideDataComparisonRespVO<TripStatisticsVO>>({
+      url: '/guide/statistics/trip/analyse',
+      params
+    })
+  },
+  // 获得商品状况明细
+  getTripStatisticsList: (params: any) => {
+    return request.get<TripStatisticsVO[]>({
+      url: '/guide/statistics/trip/list',
+      params
+    })
+  },
+  // 导出获得商品状况明细 Excel
+  exportTripStatisticsExcel: (params: any) => {
+    return request.download({
+      url: '/guide/statistics/trip/export-excel',
+      params
+    })
+  },
+  // 获得商品排行榜分页
+  getTripStatisticsRankPage: async (params: any) => {
+    return await request.get({
+      url: `/guide/statistics/trip/rank-page`,
+      params
+    })
+  }
+}

+ 80 - 0
src/api/guide/trade/afterSale/index.ts

@@ -0,0 +1,80 @@
+import request from '@/config/axios'
+
+export interface GuideTradeAfterSaleVO {
+  id?: number | null // 售后编号,主键自增
+  no?: string // 售后单号
+  status?: number | null // 退款状态
+  way?: number | null // 售后方式
+  type?: number | null // 售后类型
+  userId?: number | null // 用户编号
+  applyReason?: string // 申请原因
+  applyDescription?: string // 补充描述
+  applyPicUrls?: string[] // 补充凭证图片
+  orderId?: number | null // 交易订单编号
+  orderNo?: string // 订单流水号
+  orderItemId?: number | null // 交易订单项编号
+  guideId?: number | null //导游id
+  tripId?: number | null //行程id
+  guideName?: string // 导游 名称
+  tripTitle?: string // 行程 名称
+  bookingDate?: Date // 预约时间
+  // spuId?: number | null // 商品 SPU 编号
+  // spuName?: string // 商品 SPU 名称
+  // skuId?: number | null // 商品 SKU 编号
+  // properties?: ProductPropertiesVO[] // 属性数组
+  // picUrl?: string // 商品图片
+  // count?: number | null // 退货商品数量
+  auditTime?: Date // 审批时间
+  auditUserId?: number | null // 审批人
+  auditReason?: string // 审批备注
+  refundPrice?: number | null // 退款金额,单位:分。
+  payRefundId?: number | null // 支付退款编号
+  refundTime?: Date // 退款时间
+  // logisticsId?: number | null // 退货物流公司编号
+  // logisticsNo?: string // 退货物流单号
+  // deliveryTime?: Date // 退货时间
+  // receiveTime?: Date // 收货时间
+  // receiveReason?: string // 收货备注
+}
+
+// export interface ProductPropertiesVO {
+//   propertyId?: number | null // 属性的编号
+//   propertyName?: string // 属性的名称
+//   valueId?: number | null //属性值的编号
+//   valueName?: string // 属性值的名称
+// }
+
+// 获得交易售后分页
+export const getAfterSalePage = async (params) => {
+  return await request.get({ url: `/guide/trade/after-sale/page`, params })
+}
+
+// 获得交易售后详情
+export const getAfterSale = async (id: any) => {
+  return await request.get({ url: `/guide/trade/after-sale/get-detail?id=${id}` })
+}
+
+// 同意售后
+export const agree = async (id: any) => {
+  return await request.put({ url: `/guide/trade/after-sale/agree?id=${id}` })
+}
+
+// 拒绝售后
+export const disagree = async (data: any) => {
+  return await request.put({ url: `/guide/trade/after-sale/disagree`, data })
+}
+
+// 确认收货
+export const receive = async (id: any) => {
+  return await request.put({ url: `/guide/trade/after-sale/receive?id=${id}` })
+}
+
+// 拒绝收货
+export const refuse = async (id: any) => {
+  return await request.put({ url: `/guide/trade/after-sale/refuse?id=${id}` })
+}
+
+// 确认退款
+export const refund = async (id: any) => {
+  return await request.put({ url: `/guide/trade/after-sale/refund?id=${id}` })
+}

+ 162 - 0
src/api/guide/trade/order/index.ts

@@ -0,0 +1,162 @@
+import request from '@/config/axios'
+
+export interface GuideOrderVO {
+  // ========== 订单基本信息 ==========
+  // id?: number | null // 订单编号
+  // no?: string // 订单流水号
+  // createTime?: Date | null // 下单时间
+  // type?: number | null // 订单类型
+  // terminal?: number | null // 订单来源
+  // userId?: number | null // 用户编号
+  // userIp?: string // 用户 IP
+  // userRemark?: string // 用户备注
+  // status?: number | null // 订单状态
+  // productCount?: number | null // 购买的商品数量
+  // finishTime?: Date | null // 订单完成时间
+  // cancelTime?: Date | null // 订单取消时间
+  // cancelType?: number | null // 取消类型
+  // remark?: string // 商家备注
+
+  // // ========== 价格 + 支付基本信息 ==========
+  // payOrderId?: number | null // 支付订单编号
+  // payStatus?: boolean // 是否已支付
+  // payTime?: Date | null // 付款时间
+  // payChannelCode?: string // 支付渠道
+  // totalPrice?: number | null // 商品原价(总)
+  // discountPrice?: number | null // 订单优惠(总)
+  // deliveryPrice?: number | null // 运费金额
+  // adjustPrice?: number | null // 订单调价(总)
+  // payPrice?: number | null // 应付金额(总)
+  // // ========== 收件 + 物流基本信息 ==========
+  // deliveryType?: number | null // 发货方式
+  // pickUpStoreId?: number // 自提门店编号
+  // pickUpVerifyCode?: string // 自提核销码
+  // deliveryTemplateId?: number | null // 配送模板编号
+  // logisticsId?: number | null // 发货物流公司编号
+  // logisticsNo?: string // 发货物流单号
+  // deliveryTime?: Date | null // 发货时间
+  // receiveTime?: Date | null // 收货时间
+  // receiverName?: string // 收件人名称
+  // receiverMobile?: string // 收件人手机
+  // receiverPostCode?: number | null // 收件人邮编
+  // receiverAreaId?: number | null // 收件人地区编号
+  // receiverAreaName?: string //收件人地区名字
+  // receiverDetailAddress?: string // 收件人详细地址
+
+  // // ========== 售后基本信息 ==========
+  // afterSaleStatus?: number | null // 售后状态
+  // refundPrice?: number | null // 退款金额
+
+  // // ========== 营销基本信息 ==========
+  // couponId?: number | null // 优惠劵编号
+  // couponPrice?: number | null // 优惠劵减免金额
+  // pointPrice?: number | null // 积分抵扣的金额
+  // vipPrice?: number | null // VIP 减免金额
+
+  id?: number | null // 订单编号
+  no?: string // 订单流水号
+  createTime?: Date | null // 下单时间
+  type?: number | null // 订单类型
+  terminal?: number | null // 订单来源
+  userId?: number | null // 用户编号
+  userIp?: string // 用户 IP
+  userRemark?: string // 用户备注
+  status?: number | null // 订单状态
+  productCount?: number | null // 购买的商品数量
+  finishTime?: Date | null // 订单完成时间
+  cancelTime?: Date | null // 订单取消时间
+  cancelType?: number | null // 取消类型
+  remark?: string // 商家备注
+  commentStatus?: number | null // 是否评价
+  payOrderId?: number | null // 支付订单编号
+  payStatus?: boolean // 是否已支付
+  payTime?: Date | null // 付款时间
+  payChannelCode?: string // 支付渠道
+  totalPrice?: number | null // 商品原价(总)
+  refundStatus?: number | null // 售后状态
+  refundPrice?: number | null // 退款金额
+  items?: GuideOrderItemRespVO[] // 订单项列表
+  // 下单用户信息
+  user?: {
+    id?: number | null
+    nickname?: string
+    avatar?: string
+    mobile?: string
+  }
+  // 订单操作日志
+  logs?: GuideOrderLogRespVO[]
+}
+
+export interface GuideOrderLogRespVO {
+  content?: string
+  createTime?: Date
+  userType?: number
+}
+
+export interface GuideOrderItemRespVO {
+  // // ========== 订单项基本信息 ==========
+  // id?: number | null // 编号
+  // userId?: number | null // 用户编号
+  // orderId?: number | null // 订单编号
+  // // ========== 商品基本信息 ==========
+  // spuId?: number | null // 商品 SPU 编号
+  // spuName?: string //商品 SPU 名称
+  // skuId?: number | null // 商品 SKU 编号
+  // picUrl?: string //商品图片
+  // count?: number | null //购买数量
+  // // ========== 价格 + 支付基本信息 ==========
+  // originalPrice?: number | null //商品原价(总)
+  // originalUnitPrice?: number | null //商品原价(单)
+  // discountPrice?: number | null //商品优惠(总)
+  // payPrice?: number | null //商品实付金额(总)
+  // orderPartPrice?: number | null //子订单分摊金额(总)
+  // orderDividePrice?: number | null //分摊后子订单实付金额(总)
+  // // ========== 营销基本信息 ==========
+  // // TODO 芋艿:在捉摸一下
+  // // ========== 售后基本信息 ==========
+  // afterSaleStatus?: number | null // 售后状态
+  id?: number | null // 编号
+  userId?: number | null // 用户编号
+  orderId?: number | null // 订单编号
+  cartId?: number | null // 订单编号
+  tripId?: number | null // 订单编号
+  bookingDate?: Date | null // 预约日期
+  picUrl?: string | null //图片
+  commentStatus?: number | null // 是否评价
+  price: number | null // 导游价格
+  guideId: number | null // 导游id
+  guideName?: string | null //导游名字
+  tripTitle?: string | null //行程名字
+}
+
+/** 交易订单统计 */
+export interface GuideTradeOrderSummaryRespVO {
+  /** 订单数量 */
+  orderCount?: number
+  /** 订单金额 */
+  orderPayPrice?: string
+  /** 退款单数 */
+  afterSaleCount?: number
+  /** 退款金额 */
+  afterSalePrice?: string
+}
+
+// 查询交易订单列表
+export const getOrderPage = async (params: any) => {
+  return await request.get({ url: `/guide/trade/order/page`, params })
+}
+
+// 查询交易订单统计
+export const getOrderSummary = async (params: any) => {
+  return await request.get<GuideTradeOrderSummaryRespVO>({ url: `/guide/trade/order/summary`, params })
+}
+
+// 查询交易订单详情
+export const getOrder = async (id: number | null) => {
+  return await request.get({ url: `/guide/trade/order/get-detail?id=` + id })
+}
+
+// 订单备注
+export const updateOrderRemark = async (data: any) => {
+  return await request.put({ url: `/guide/trade/order/update-remark`, data })
+}

+ 100 - 0
src/api/guide/trip/index.ts

@@ -0,0 +1,100 @@
+import request from '@/config/axios'
+
+// 旅程基本情报 VO
+export interface TripVO {
+  id: number // 旅程编号
+  guideId: number // 导游编号
+  startTime: Date // 旅程开始时间
+  endTime: Date // 旅程结束时间
+  maximumNumber: number // 服务对象人数上限
+  price: number // 导游服务价格
+  status: number // 开启状态
+}
+
+// 旅程基本情报 API
+export const TripApi = {
+  // 查询旅程基本情报分页
+  getTripPage: async (params: any) => {
+    return await request.get({ url: `/guide/trip/page`, params })
+  },
+
+  // 查询旅程基本情报详情
+  getTrip: async (id: number) => {
+    return await request.get({ url: `/guide/trip/get?id=` + id })
+  },
+
+  // 新增旅程基本情报
+  createTrip: async (data: TripVO) => {
+    return await request.post({ url: `/guide/trip/create`, data })
+  },
+
+  // 修改旅程基本情报
+  updateTrip: async (data: TripVO) => {
+    return await request.put({ url: `/guide/trip/update`, data })
+  },
+
+  // 删除旅程基本情报
+  deleteTrip: async (id: number) => {
+    return await request.delete({ url: `/guide/trip/delete?id=` + id })
+  },
+
+  // 导出旅程基本情报 Excel
+  exportTrip: async (params) => {
+    return await request.download({ url: `/guide/trip/export-excel`, params })
+  },
+  // 查询旅程基本情报详情
+  getTripCount: async () => {
+    return await request.get({ url: `/guide/trip/get-count`})
+  },
+// ==================== 子表(旅程多语言扩充信息) ====================
+
+  // 获得旅程多语言扩充信息分页
+  getTripI18nExtensionPage: async (params) => {
+    return await request.get({ url: `/guide/trip/trip-i18n-extension/page`, params })
+  },
+  // 新增旅程多语言扩充信息
+  createTripI18nExtension: async (data) => {
+    return await request.post({ url: `/guide/trip/trip-i18n-extension/create`, data })
+  },
+
+  // 修改旅程多语言扩充信息
+  updateTripI18nExtension: async (data) => {
+    return await request.put({ url: `/guide/trip/trip-i18n-extension/update`, data })
+  },
+
+  // 删除旅程多语言扩充信息
+  deleteTripI18nExtension: async (id: number) => {
+    return await request.delete({ url: `/guide/trip/trip-i18n-extension/delete?id=` + id })
+  },
+
+  // 获得旅程多语言扩充信息
+  getTripI18nExtension: async (id: number) => {
+    return await request.get({ url: `/guide/trip/trip-i18n-extension/get?id=` + id })
+  },
+
+// ==================== 子表(旅程详细情报) ====================
+
+  // 获得旅程详细情报分页
+  getTripSightsPage: async (params) => {
+    return await request.get({ url: `/guide/trip/trip-sights/page`, params })
+  },
+  // 新增旅程详细情报
+  createTripSights: async (data) => {
+    return await request.post({ url: `/guide/trip/trip-sights/create`, data })
+  },
+
+  // 修改旅程详细情报
+  updateTripSights: async (data) => {
+    return await request.put({ url: `/guide/trip/trip-sights/update`, data })
+  },
+
+  // 删除旅程详细情报
+  deleteTripSights: async (id: number) => {
+    return await request.delete({ url: `/guide/trip/trip-sights/delete?id=` + id })
+  },
+
+  // 获得旅程详细情报
+  getTripSights: async (id: number) => {
+    return await request.get({ url: `/guide/trip/trip-sights/get?id=` + id })
+  }
+}

+ 92 - 0
src/api/guide/users/index.ts

@@ -0,0 +1,92 @@
+import request from '@/config/axios'
+
+// 导游信息 VO
+export interface UsersVO {
+  id: number // 用户ID
+  username: string // 用户账号
+  nickname: string
+  password: string
+  deptId: number // 部门ID
+  email: string // 用户邮箱
+  mobile: string // 手机号码
+  sex: number // 用户性别
+  avatar: string // 头像地址
+  languageType: number // 导游语言
+  selfIntroduction: string // 图文自我介绍(多语言对应的json格式)
+  categoryIds: string // 擅长领域
+  serviceItems: string // 服务项目(多语言json)
+  memo: string // 提供服务时的注意事项(包含什么,不包含什么多语言json)
+  price: number // 导游服务价格
+  maximumNumber: number // 服务对象人数上限
+  geographicalIds: string // 地域
+  startDate: Date // 导游服务开始日
+  endDate: Date // 导游服务终了日
+  bankInfo: string // 银行信息(后期需要扩展,平台和导游月底结账用)
+}
+
+// 导游信息 API
+export const UsersApi = {
+  // 查询导游信息分页
+  getUsersPage: async (params: any) => {
+    return await request.get({ url: `/guide/users/page`, params })
+  },
+
+  // 查询导游信息详情
+  getUsers: async (id: number) => {
+    return await request.get({ url: `/guide/users/get?id=` + id })
+  },
+
+  // 新增导游信息
+  createUsers: async (data: UsersVO) => {
+    return await request.post({ url: `/guide/users/create`, data })
+  },
+
+  // 修改导游信息
+  updateUsers: async (data: UsersVO) => {
+    return await request.put({ url: `/guide/users/update`, data })
+  },
+
+  // 删除导游信息
+  deleteUsers: async (id: number) => {
+    return await request.delete({ url: `/guide/users/delete?id=` + id })
+  },
+
+  // 导出导游信息 Excel
+  exportUsers: async (params) => {
+    return await request.download({ url: `/guide/users/export-excel`, params })
+  },
+  // 查询导游信息详情
+  getGuideCount: async () => {
+    return await request.get({ url: `/guide/users/get-count` })
+  },
+// ==================== 子表(导游评论) ====================
+
+  // 获得导游评论分页
+  getCommentPage: async (params) => {
+    return await request.get({ url: `/guide/users/comment/page`, params })
+  },
+  // 新增导游评论
+  createComment: async (data) => {
+    return await request.post({ url: `/guide/users/comment/create`, data })
+  },
+
+  // 修改导游评论
+  updateComment: async (data) => {
+    return await request.put({ url: `/guide/users/comment/update`, data })
+  },
+
+  // 删除导游评论
+  deleteComment: async (id: number) => {
+    return await request.delete({ url: `/guide/users/comment/delete?id=` + id })
+  },
+
+  // 获得导游评论
+  getComment: async (id: number) => {
+    return await request.get({ url: `/guide/users/comment/get?id=` + id })
+  },
+
+   // 获得导游评论
+  getSelfComment: async () => {
+    return await request.get({ url: `/guide/users/comment/self/get`})
+  }
+}

+ 37 - 1
src/api/system/user/profile.ts

@@ -29,6 +29,22 @@ export interface ProfileVO {
   loginIp: string
   loginDate: Date
   createTime: Date
+  languageType: number // 导游语言
+  selfIntroduction: string // 图文自我介绍
+  categoryIds: [] // 擅长领域
+  serviceItems: string // 服务项目
+  memo: string // 注意事项
+  price: number // 导游服务价格
+  maximumNumber: number // 服务对象人数上限
+  geographicalIds: [] // 地域
+  startDate: Date // 导游服务开始日
+  endDate: Date // 导游服务终了日
+  bankInfo: string // 银行信息(后期需要扩展,平台和导游月底结账用)
+  wechat: string //wechat QR
+  facebook: string //facebook QR
+  whatsapp: string //whatsapp QR
+  line: string //line QR
+
 }
 
 export interface UserProfileUpdateReqVO {
@@ -37,7 +53,23 @@ export interface UserProfileUpdateReqVO {
   mobile: string
   sex: number
 }
-
+export interface UserProfileUpdateGuideInfoReqVO {
+  languageType: number // 导游语言
+  selfIntroduction: string // 图文自我介绍
+  categoryIds: [] // 擅长领域
+  serviceItems: string // 服务项目
+  memo: string // 注意事项
+  price: number // 导游服务价格
+  maximumNumber: number // 服务对象人数上限
+  geographicalIds: [] // 地域
+  startDate: Date // 导游服务开始日
+  endDate: Date // 导游服务终了日
+  bankInfo: string // 银行信息(后期需要扩展,平台和导游月底结账用)
+  wechat: string //wechat QR
+  facebook: string //facebook QR
+  whatsapp: string //whatsapp QR
+  line: string //line QR
+}
 // 查询用户个人信息
 export const getUserProfile = () => {
   return request.get({ url: '/system/user/profile/get' })
@@ -48,6 +80,10 @@ export const updateUserProfile = (data: UserProfileUpdateReqVO) => {
   return request.put({ url: '/system/user/profile/update', data })
 }
 
+// 修改用户个人信息
+export const updateUserProfileGuideInfo = (data: UserProfileUpdateGuideInfoReqVO) => {
+  return request.put({ url: '/system/user/profile/update-guide-info', data })
+}
 // 用户密码重置
 export const updateUserPassword = (oldPassword: string, newPassword: string) => {
   return request.put({

BIN
src/assets/imgs/image-panda-removebg-preview.png


+ 1 - 1
src/components/AppLinkInput/data.ts

@@ -59,7 +59,7 @@ export const APP_LINK_GROUP_LIST = [
       },
       {
         name: '个人中心',
-        path: '/pages/index/user'
+        path: '/guide-profile/profile/'
       },
       {
         name: '商品搜索',

+ 3 - 0
src/components/QuillEditor/index.ts

@@ -0,0 +1,3 @@
+import QEditor from './src/QEditor.vue'
+
+export { QEditor }

+ 255 - 0
src/components/QuillEditor/src/QEditor.vue

@@ -0,0 +1,255 @@
+<template>
+  <div>
+    <el-upload
+      :action="uploadUrl"
+      :before-upload="handleBeforeUpload"
+      :on-success="handleUploadSuccess"
+      :on-error="handleUploadError"
+      name="file"
+      :show-file-list="false"
+      :headers="headers"
+      class="editor-img-uploader"
+      v-if="type == 'url'"
+    >
+      <i ref="uploadRef" class="editor-img-uploader"></i>
+    </el-upload>
+  </div>
+  <div class="editor">
+    <quill-editor
+      ref="quillEditorRef"
+      v-model:content="content"
+      contentType="html"
+      @text-change="(e) => $emit('update:modelValue', content)"
+      :options="options"
+      :style="styles"
+    />
+  </div>
+</template>
+
+<script setup>
+import { QuillEditor } from "@vueup/vue-quill";
+import "@vueup/vue-quill/dist/vue-quill.snow.css";
+import { getAccessToken,getTenantId } from "@/utils/auth";
+
+const { proxy } = getCurrentInstance();
+
+defineOptions({ name: 'QEditor' })
+
+const quillEditorRef = ref();
+const uploadUrl = ref(import.meta.env.VITE_UPLOAD_URL); // 上传的图片服务器地址
+const headers = ref({
+  Authorization: "Bearer " + getAccessToken(),
+  'tenant-id': getTenantId()
+});
+
+const props = defineProps({
+  /* 编辑器的内容 */
+  modelValue: {
+    type: String,
+  },
+  /* 高度 */
+  height: {
+    type: Number,
+    default: null,
+  },
+  /* 最小高度 */
+  minHeight: {
+    type: Number,
+    default: null,
+  },
+  /* 只读 */
+  readOnly: {
+    type: Boolean,
+    default: false,
+  },
+  /* 上传文件大小限制(MB) */
+  fileSize: {
+    type: Number,
+    default: 5,
+  },
+  /* 类型(base64格式、url格式) */
+  type: {
+    type: String,
+    default: "url",
+  }
+});
+
+const options = ref({
+  theme: "snow",
+  bounds: document.body,
+  debug: "warn",
+  modules: {
+    // 工具栏配置
+    toolbar: [
+      ["bold", "italic", "underline", "strike"],      // 加粗 斜体 下划线 删除线
+      ["blockquote", "code-block"],                   // 引用  代码块
+      [{ list: "ordered" }, { list: "bullet" }],      // 有序、无序列表
+      [{ indent: "-1" }, { indent: "+1" }],           // 缩进
+      [{ size: ["small", false, "large", "huge"] }],  // 字体大小
+      [{ header: [1, 2, 3, 4, 5, 6, false] }],        // 标题
+      [{ color: [] }, { background: [] }],            // 字体颜色、字体背景颜色
+      [{ align: [] }],                                // 对齐方式
+      ["clean"],                                      // 清除文本格式
+      ["link", "image", "video"]                      // 链接、图片、视频
+    ],
+  },
+  placeholder: "请输入内容",
+  readOnly: props.readOnly
+});
+
+const styles = computed(() => {
+  let style = {};
+  if (props.minHeight) {
+    style.minHeight = `${props.minHeight}px`;
+  }
+  if (props.height) {
+    style.height = `${props.height}px`;
+  }
+  return style;
+});
+
+const content = ref("");
+watch(() => props.modelValue, (v) => {
+  if (v !== content.value) {
+    content.value = v === undefined ? "<p></p>" : v;
+  }
+}, { immediate: true });
+
+// 如果设置了上传地址则自定义图片上传事件
+onMounted(() => {
+  if (props.type == 'url') {
+    let quill = quillEditorRef.value.getQuill();
+    let toolbar = quill.getModule("toolbar");
+    toolbar.addHandler("image", (value) => {
+      if (value) {
+        proxy.$refs.uploadRef.click();
+      } else {
+        quill.format("image", false);
+      }
+    });
+  }
+});
+
+// 上传前校检格式和大小
+function handleBeforeUpload(file) {
+  const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
+  const isJPG = type.includes(file.type);
+  //检验文件格式
+  if (!isJPG) {
+    proxy.$modal.msgError(`图片格式错误!`);
+    return false;
+  }
+  // 校检文件大小
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
+      return false;
+    }
+  }
+  return true;
+}
+
+// 上传成功处理
+function handleUploadSuccess(res, file) {
+  // 如果上传成功
+  if (res.code == 0) {
+    // 获取富文本实例
+    let quill = toRaw(quillEditorRef.value).getQuill();
+    // 获取光标位置
+    let length = quill.selection.savedRange.index;
+    // 插入图片,res.url为服务器返回的图片链接地址
+    // quill.insertEmbed(length, "image", import.meta.env.VITE_APP_BASE_API + res.fileName);
+    quill.insertEmbed(length, "image",  res.data);
+    // 调整光标到最后
+    quill.setSelection(length + 1);
+  } else {
+    proxy.$modal.msgError("图片插入失败");
+  }
+}
+
+// 上传失败处理
+function handleUploadError() {
+  proxy.$modal.msgError("图片插入失败");
+}
+</script>
+
+<style>
+.editor-img-uploader {
+  display: none;
+}
+.editor, .ql-toolbar {
+  white-space: pre-wrap !important;
+  line-height: normal !important;
+}
+.quill-img {
+  display: none;
+}
+.ql-snow .ql-tooltip[data-mode="link"]::before {
+  content: "请输入链接地址:";
+}
+.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
+  border-right: 0px;
+  content: "保存";
+  padding-right: 0px;
+}
+.ql-snow .ql-tooltip[data-mode="video"]::before {
+  content: "请输入视频地址:";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item::before {
+  content: "14px";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
+  content: "10px";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
+  content: "18px";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
+  content: "32px";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item::before {
+  content: "文本";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+  content: "标题1";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+  content: "标题2";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+  content: "标题3";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+  content: "标题4";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+  content: "标题5";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+  content: "标题6";
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item::before {
+  content: "标准字体";
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
+  content: "衬线字体";
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
+  content: "等宽字体";
+}
+</style>

+ 5 - 6
src/components/ShortcutDateRangePicker/index.vue

@@ -1,16 +1,16 @@
 <template>
   <div class="flex flex-row items-center gap-2">
     <el-radio-group v-model="shortcutDays" @change="handleShortcutDaysChange">
-      <el-radio-button :label="1">昨</el-radio-button>
-      <el-radio-button :label="7">最近7</el-radio-button>
-      <el-radio-button :label="30">最近30</el-radio-button>
+      <el-radio-button :label="1">昨</el-radio-button>
+      <el-radio-button :label="7">最近7日間</el-radio-button>
+      <el-radio-button :label="30">最近30日間</el-radio-button>
     </el-radio-group>
     <el-date-picker
       v-model="times"
       value-format="YYYY-MM-DD HH:mm:ss"
       type="daterange"
-      start-placeholder="开始日期"
-      end-placeholder="结束日期"
+      start-placeholder="開始日"
+      end-placeholder="終了日"
       :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
       :shortcuts="shortcuts"
       class="!w-240px"
@@ -25,7 +25,6 @@ import * as DateUtil from '@/utils/formatTime'
 
 /** 快捷日期范围选择组件 */
 defineOptions({ name: 'ShortcutDateRangePicker' })
-
 const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天
 const times = ref<[string, string]>(['', '']) // 时间范围参数
 defineExpose({ times }) // 暴露时间范围参数

+ 1 - 1
src/layout/components/Footer/src/Footer.vue

@@ -19,6 +19,6 @@ const title = computed(() => appStore.getTitle)
     :class="prefixCls"
     class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]"
   >
-    <span class="text-14px">Copyright ©2022-{{ title }}</span>
+    <span class="text-14px">Copyright ©2024-パンダシステムズ株式会社</span>
   </div>
 </template>

+ 4 - 3
src/layout/components/Logo/src/Logo.vue

@@ -4,7 +4,7 @@ import { useAppStore } from '@/store/modules/app'
 import { useDesign } from '@/hooks/web/useDesign'
 
 defineOptions({ name: 'Logo' })
-
+const { t } = useI18n()
 const { getPrefixCls } = useDesign()
 
 const prefixCls = getPrefixCls('logo')
@@ -68,7 +68,7 @@ watch(
     >
       <img
         class="h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
-        src="@/assets/imgs/logo.png"
+        src="@/assets/imgs/image-panda-removebg-preview.png"
       />
       <div
         v-if="show"
@@ -81,7 +81,8 @@ watch(
           }
         ]"
       >
-        {{ title }}
+      Panda Systems
+        <!-- {{ t('common.systemName') }} -->
       </div>
     </router-link>
   </div>

+ 3 - 3
src/layout/components/UserInfo/src/UserInfo.vue

@@ -47,7 +47,7 @@ const loginOut = async () => {
   } catch {}
 }
 const toProfile = async () => {
-  push('/user/profile')
+  push('/guide-profile/profile')
 }
 const toDocument = () => {
   window.open('https://doc.iocoder.cn/')
@@ -68,10 +68,10 @@ const toDocument = () => {
           <Icon icon="ep:tools" />
           <div @click="toProfile">{{ t('common.profile') }}</div>
         </ElDropdownItem>
-        <ElDropdownItem>
+        <!-- <ElDropdownItem>
           <Icon icon="ep:menu" />
           <div @click="toDocument">{{ t('common.document') }}</div>
-        </ElDropdownItem>
+        </ElDropdownItem> -->
         <ElDropdownItem divided>
           <Icon icon="ep:lock" />
           <div @click="lockScreen">{{ t('lock.lockScreen') }}</div>

+ 2 - 2
src/layout/components/UserInfo/src/components/LockDialog.vue

@@ -72,12 +72,12 @@ const handleLock = async () => {
         {{ userName }}
       </span>
     </div>
-    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
       <el-form-item :label="t('lock.lockPassword')" prop="password">
         <el-input
           type="password"
           v-model="formData.password"
-          :placeholder="'请输入' + t('lock.lockPassword')"
+          :placeholder="t('lock.placeholder')"
           clearable
           show-password
         />

+ 688 - 9
src/locales/en.ts

@@ -1,5 +1,7 @@
 export default {
   common: {
+    systemName: 'Traveler and Guide Matching System',
+    travelerDetail: 'Traveler Detail Infomation',
     inputText: 'Please input',
     selectText: 'Please select',
     startTimeText: 'Start time',
@@ -326,7 +328,6 @@ export default {
       networkException: 'network anomaly',
       networkExceptionMsg:
         'Please check if your network connection is normal! The network is abnormal',
-
       errMsg401: 'The user does not have permission (token, user name, password error)!',
       errMsg403: 'The user is authorized, but access is forbidden!',
       errMsg404: 'Network request error, the resource was not found!',
@@ -370,22 +371,18 @@ export default {
       signInFormTitle: 'Sign in',
       signUpFormTitle: 'Sign up',
       forgetFormTitle: 'Reset password',
-
       signInTitle: 'Backstage management system',
       signInDesc: 'Enter your personal details and get started!',
       policy: 'I agree to the xxx Privacy Policy',
       scanSign: `scanning the code to complete the login`,
-
       loginButton: 'Sign in',
       registerButton: 'Sign up',
       rememberMe: 'Remember me',
       forgetPassword: 'Forget Password?',
       otherSignIn: 'Sign in with',
-
       // notify
       loginSuccessTitle: 'Login successful',
       loginSuccessDesc: 'Welcome back',
-
       // placeholder
       accountPlaceholder: 'Please input username',
       passwordPlaceholder: 'Please input password',
@@ -393,7 +390,6 @@ export default {
       mobilePlaceholder: 'Please input mobile',
       policyPlaceholder: 'Register after checking',
       diffPwd: 'The two passwords are inconsistent',
-
       userName: 'Username',
       password: 'Password',
       confirmPassword: 'Confirm Password',
@@ -418,10 +414,33 @@ export default {
       createTime: 'Created Date'
     },
     info: {
-      title: 'Basic Information',
+      title: 'Guide Information',
       basicInfo: 'Basic Information',
       resetPwd: 'Reset Password',
-      userSocial: 'Social Information'
+      userSocial: 'Social Information',
+      commentInfo: 'Comment Information',
+      Schedule: 'Schedule Information',
+      languageUsed: 'Language Used',
+      textAndImageIntro: 'Text and Image Introduction',
+      expertiseField: 'Area of Expertise',
+      serviceItems: 'Service Items',
+      precautions: 'Precautions',
+      servicePrice: 'Service Price',
+      maxParticipants: 'Maximum Number of Participants',
+      region: 'Region',
+      serviceStartDate: 'Guide Service Start Date',
+      serviceEndDate: 'Guide Service End Date',
+      settlementBankInfo: 'Settlement Bank Information',
+      ratingStars: 'Rating Stars',
+      expertiseStars: 'Expertise Stars',
+      serviceStars: 'Service Stars',
+      reviewContent: 'Review Content',
+      likesCount: 'Likes Count',
+      work: 'Work',
+      rest: 'Rest',
+      booked: 'Booked',
+      modifyDialog1: 'Cannot modify dates prior to today',
+      modifyDialog2: 'This date has been booked and cannot be modified.'
     },
     rules: {
       nickname: 'Please Enter User Nickname',
@@ -453,5 +472,665 @@ export default {
     btn_zoom_in: 'Zoom in',
     btn_zoom_out: 'Zoom out',
     preview: 'Preivew'
-  }
+  },
+  'OAuth 2.0': 'OAuth 2.0', // メニュー名が OAuth 2.0 の場合、常に警告が表示されないようにする
+  guide: {
+    common: {
+      search: 'Search',
+      reset: 'Reset',
+      add: 'Add',
+      export: 'Export',
+      expandCollapse: 'Expand/Collapse',
+      actions: 'Actions',
+      edit: 'Edit',
+      delete: 'Delete',
+      confirm: 'Confirm',
+      cancel: 'Cancel',
+    },
+    category: {
+      chineseName: 'Chinese Name',
+      enterChineseName: 'Please enter the Chinese name for the category',
+      japaneseName: 'Japanese Name',
+      enterJapaneseName: 'Please enter the Japanese name for the category',
+      englishName: 'English Name',
+      enterEnglishName: 'Please enter the English name for the category',
+      creationTime: 'Creation Time',
+      startDate: 'Start Date',
+      endDate: 'End Date',
+      parentCategoryID: 'Parent Category ID',
+      categoryID: 'Category ID',
+      mobileCategoryImage: 'Mobile Category Image',
+      categorySort: 'Category Sort',
+      enableStatus: 'Enable Status',
+      selectParentCategoryID: 'Please select the Parent Category ID',
+      enterCategorySort: 'Please enter the category sort',
+      topTouristAttractionCategory: 'Top Tourist Attraction Category'
+    },
+    sight:{
+      id: 'ID',
+      baseInfo: 'Base Information',
+      attractionCategory: 'Category',
+      region: 'Region',
+      attractionInfo: 'Attraction Information',
+      phoneNumber: 'Phone Number',
+      openingHours: 'Opening Hours',
+      enterOpeningHours: 'Enter Opening Hours',
+      multilingualExpansionInfo: 'Multilingual Expansion Information',
+      comments: 'Comments',
+      attractionCoverImage: 'Attraction Cover Image',
+      enterPhoneNumber: 'Enter Phone Number',
+      attractionCarouselImages: 'Carousel Images, up to 6',
+      officialWebsiteURL: 'Official Website URL',
+      enterOfficialWebsiteURL: 'Enter Official Website URL',
+      playTime: 'Play Time',
+      enterApproximatePlayTime: 'Enter Approximate Play Time',
+      sort: 'Sort',
+      selectATouristAttraction: 'Select a Tourist Attraction',
+      selectAttractionCategory: 'Select Attraction Category',
+      ratingStars: 'Rating Stars',
+      sceneryStars: 'Scenery Stars',
+      serviceStars: 'Service Stars',
+      reviewContent: 'Review Content',
+      tourDuration: 'Tour Duration',
+      likesCount: 'Likes Count',
+      enterLikesCount: 'Enter Likes Count',
+      image: 'Image',
+      multilingualType: 'Multilingual Type',
+      title: 'Title',
+      introduction: 'Introduction',
+      ticketsAndReservations: 'Tickets & Reservations',
+      keywordGroup: 'Keyword Group',
+      enterKeywords: 'Enter Keywords',
+      subtitle: 'Subtitle',
+      attractionTextualDescription: 'Attraction Textual Description',
+      address: 'Address',
+      additionalInfo: 'Additional Information'
+    },
+    trip:{
+      guideId: 'Guide ID',
+      enterGuideId: 'Enter Guide ID',
+      creationTime: 'Creation Time',
+      startDate: 'Start Date',
+      endDate: 'End Date',
+      id: 'ID',
+      guide: 'Guide',
+      coverImage: 'Cover Image',
+      maxParticipants: 'Max Participants',
+      guidePrice: 'Guide Price',
+      multilingualExpansionInfo: 'Multilingual Expansion Info',
+      includedAttractions: 'Included Attractions',
+      startTime: 'Start Time',
+      selectJourneyStartTime: 'Select Journey Start Time',
+      endTime: 'End Time',
+      selectJourneyEndTime: 'Select Journey End Time',
+      guideServicePrice: 'Guide Service Price',
+      enterGuideServicePrice: 'Enter Guide Service Price',
+      enableStatus: 'Enable Status',
+      importAttractions: 'Import Attractions',
+      attractionCategory: 'Attraction Category',
+      region: 'Region',
+      attractionInfo: 'Attraction Information',
+      openingHours: 'Opening Hours',
+      sightseeingSpotId: 'Sightseeing Spot ID',
+      journeyStartTime: 'Journey Start Time',
+      requiredTime: 'Required Time',
+      selectJourneyBasicInfo: 'Select Journey Basic Info',
+      multilingualType: 'Multilingual Type',
+      multilingualTitle: 'Multilingual Title',
+      includedItems: 'Included Items',
+      excludedItems: 'Excluded Items',
+      featureTextualDescription: 'Feature Textual Description',
+      additionalInfo: 'Additional Information'
+    },
+    user:{
+      userAccount: 'User Account',
+      enterUserAccount: 'Enter User Account',
+      mobileNumber: 'Mobile Number',
+      enterMobileNumber: 'Enter Mobile Number',
+      guideLanguage: 'Language',
+      selectGuideLanguage: 'Select Guide Language',
+      creationTime: 'Creation Time',
+      startDate: 'Start Date',
+      endDate: 'End Date',
+      userId: 'User ID',
+      userNickname: 'User Nickname',
+      lastLoginTime: 'Last Login Time',
+      expertiseField: 'Expertise Field',
+      guideReviews: 'Guide Reviews',
+      userPassword: 'User Password',
+      enterUserPassword: 'Enter User Password',
+      enterUserNickname: 'Enter User Nickname',
+      userEmail: 'User Email',
+      enterUserEmail: 'Enter User Email',
+      userGender: 'User Gender',
+      pleaseSelect: 'Please Select',
+      selectGuideUsageLanguage: 'Select Guide Usage Language',
+      textAndImageIntro: 'Text and Image Introduction',
+      serviceItems: 'Service Items',
+      enterServiceItems: 'Enter Service Items',
+      precautions: 'Precautions',
+      enterServicePrecautions: 'Enter Service Precautions',
+      guideServicePrice: 'Guide Service Price',
+      maxNumberOfPeople: 'Max Number of People',
+      serviceTargetMaxNumber: 'Service Target Max Number',
+      region: 'Region',
+      guideServiceStartDate: 'Guide Service Start Date',
+      selectGuideServiceStartDate: 'Select Guide Service Start Date',
+      guideServiceEndDate: 'Guide Service End Date',
+      selectGuideServiceEndDate: 'Select Guide Service End Date',
+      bankInformation: 'Bank Information',
+      enterBankInformation: 'Enter Bank Information',
+      enterCorrectEmail: 'Enter Correct Email',
+      enterCorrectMobileNumber: 'Enter Correct Mobile Number',
+      id: 'ID',
+      reviewer: 'Reviewer',
+      type: 'Type',
+      ratingStars: 'Rating Stars',
+      expertiseStars: 'Expertise Stars',
+      serviceStars: 'Service Stars',
+      reviewTime: 'Review Time',
+      reviewerId: 'Reviewer ID',
+      enterReviewerUserId: 'Enter Reviewer User ID',
+      enterReviewerName: 'Enter Reviewer Name',
+      selectCustomerReviewSelfReview: 'Select Customer Review/Self Review',
+      reviewContent: 'Review Content',
+      likesCount: 'Likes Count',
+      registrationTime: 'Registration Time',
+      loginTime: 'Login Time',
+      avatar: 'Avatar',
+      status: 'Status',
+      realName: 'Real Name',
+      enterRealName: 'Enter Real Name',
+      birthDate: 'Birth Date',
+      selectBirthDate: 'Select Birth Date',
+      basicInformation: 'Basic Information',
+      accountDetails: 'Account Details',
+      orderManagement: 'Order Management',
+      afterSalesManagement: 'After Sales Management',
+      favoritesRecord: 'Favorites Record',
+      username: 'Username',
+      location: 'Location',
+      registrationIP: 'Registration IP',
+    },
+    order:{
+      merchantOrderNumber: 'Order Number',
+      enterMerchantOrderNumber: 'Enter Order Number',
+      paymentOrderNumber: 'Payment Order Number',
+      enterPaymentOrderNumber: 'Enter Payment Order Number',
+      paymentStatus: 'Payment Status',
+      selectPaymentStatus: 'Select Payment Status',
+      creationTime: 'Creation Time',
+      startDate: 'Start Date',
+      endDate: 'End Date',
+      id: 'ID',
+      paymentAmount: 'Payment Amount',
+      refundAmount: 'Refund Amount',
+      handlingFee: 'Handling Fee',
+      orderNumber: 'Order Number',
+      merchant: 'OrderId',
+      payment: 'Payment Order Number',
+      channel: 'Channel',
+      paymentTime: 'Payment Time',
+      productTitle: 'Product Title',
+      orderDetails: 'Order Details',
+      handlingFeeRate: 'Handling Fee Rate',
+      expirationTime: 'Expiration Time',
+      updateTime: 'Update Time',
+      successTime: 'Success Time',
+      productDescription: 'Product Description',
+      paymentIP: 'Payment IP',
+      notificationURL: 'Notification URL',
+      asyncCallbackContent: 'Payment Channel Asynchronous Callback Content',
+      refundOrderNumber: 'Refund Order Number',
+      enterRefundOrderNumber: 'Enter Refund Order Number',
+      refundStatus: 'Refund Status',
+      selectRefundStatus: 'Select Refund Status',
+      details: 'Details',
+      refundTime: 'Refund Time',
+      refundReason: 'Refund Reason',
+      refundIP: 'Refund IP',
+      channelErrorCode: 'Channel Error Code',
+      orderStatus: 'Order Status',
+      all: 'All',
+      customTime: 'Custom Time',
+      aggregateSearch: 'Aggregate Search',
+      enter: 'Please Enter',
+      remark: 'Remark',
+      userUID: 'User UID',
+      userNickname: 'User Nickname',
+      userPhone: 'User Phone',
+      merchantRemark: 'Merchant Remark',
+      enterOrderRemark: 'Enter Order Remark',
+      reservationInfo: 'Reservation Info',
+      reservationUnitPrice: 'Reservation Unit Price/Guide Date',
+      afterSaleStatus: 'After-Sale Status',
+      orderPrice: 'Order Price',
+      tourist: 'Tourist',
+      orderTime: 'Order Time',
+      paymentMethod: 'Payment Method',
+      yen: 'Yen',
+      actualPayment: 'Actual Payment',
+      orderInfo: 'Order Info',
+      buyer: 'Buyer',
+      orderType: 'Order Type',
+      orderSource: 'Order Source',
+      buyerMessage: 'Buyer Message',
+      reservationGuideInfo: 'Reservation Guide Info',
+      totalAmountOfGoods: 'Total Amount of Goods',
+      totalPayment: 'Total Payment',
+      visitorInfo: 'Visitor Info',
+      orderOperationLog: 'Order Operation Log',
+      notpay: 'Not Paid',
+      reservationdate: 'Reservation Date'
+    },
+    infra:{
+      configName: 'Config Name',
+      enterConfigName: 'Enter Config Name',
+      storage: 'Storage',
+      selectStorage: 'Select Storage',
+      creationTime: 'Creation Time',
+      startDate: 'Start Date',
+      endDate: 'End Date',
+      id: 'ID',
+      remark: 'Remark',
+      mainConfig: 'Main Config',
+      enterRemark: 'Enter Remark',
+      basePath: 'Base Path',
+      enterBasePath: 'Enter Base Path',
+      hostAddress: 'Host Address',
+      enterHostAddress: 'Enter Host Address',
+      hostPort: 'Host Port',
+      enterHostPort: 'Enter Host Port',
+      username: 'Username',
+      enterUsername: 'Enter Username',
+      enterPassword: 'Enter Password',
+      password: 'Password',
+      activeMode: 'Active Mode',
+      passiveMode: 'Passive Mode',
+      nodeAddress: 'Node Address',
+      enterNodeAddress: 'Enter Node Address',
+      storageBucket: 'Storage Bucket',
+      enterBucket: 'Enter Bucket',
+      enterAccessKey: 'Enter Access Key',
+      enterAccessSecret: 'Enter Access Secret',
+      customDomain: 'Custom Domain',
+      filePath: 'File Path',
+      enterFilePath: 'Enter File Path',
+      fileType: 'File Type',
+      enterFileType: 'Enter File Type',
+      uploadFile: 'Upload File',
+      fileSize: 'File Size',
+      fileName: 'File Name',
+      fileContent: 'File Content',
+      preview: 'Preview',
+      download: 'Download',
+      uploadTime: 'Upload Time',
+      userId: 'User ID',
+      enterUserId: 'Enter User ID',
+      executionDuration: 'Execution Duration',
+      requestTime: 'Request Time',
+      enterExecutionDuration: 'Enter Execution Duration',
+      resultCode: 'Result Code',
+      enterResultCode: 'Enter Result Code',
+      logId: 'Log ID',
+      requestMethod: 'Request Method',
+      requestAddress: 'Request Address',
+      operationResult: 'Operation Result',
+      success: 'Success',
+      failure: 'Failure',
+      operationModule: 'Operation Module',
+      operationName: 'Operation Name',
+      operationType: 'Operation Type',
+      operation: 'Operation',
+      logPrimaryKey: 'Log Primary Key',
+      linkTracing: 'Link Tracing',
+      userIP: 'User IP',
+      userInfo: 'User Info',
+      userUA: 'User UA',
+      requestInfo: 'Request Info',
+      requestParams: 'Request Params',
+      requestResult: 'Request Result',
+      requestDuration: 'Request Duration',
+      normal: 'Normal',
+      exceptionTime: 'Exception Time',
+      processingStatus: 'Processing Status',
+      selectProcessingStatus: 'Select Processing Status',
+      exceptionOccurrenceTime: 'Exception Occurrence Time',
+      exceptionName: 'Exception Name',
+      handler: 'Handler',
+      redisVersion: 'Redis Version',
+      runningMode: 'Running Mode',
+      standalone: 'Standalone',
+      cluster: 'Cluster',
+      port: 'Port',
+      clientCount: 'Client Count',
+      runningDays: 'Running Days',
+      usedMemory: 'Used Memory',
+      usedCPU: 'Used CPU',
+      memoryConfig: 'Memory Config',
+      isAOFEnabled: 'Is AOF Enabled',
+      isRDBSuccessful: 'Is RDB Successful',
+      keyCount: 'Key Count',
+      networkInOut: 'Network In/Out',
+      memoryUsage: 'Memory Usage',
+      peak: 'Peak',
+      commandStats: 'Command Stats',
+      command: 'Command',
+      memoryConsumption: 'Memory Consumption',
+      processed:'Processed',
+      ignored:'Ignored',
+      exceptionStack: 'Exception Stack Trace',
+      test: 'Test',
+      dragFilesHere: 'Drag files here, or',
+      clickToUpload: 'click to upload',
+      tipOnlyJPGPNGGIF: 'Tip: Only jpg, png, gif format files are allowed!',
+      uploadFailed: 'Upload failed, please try again!',
+      onlyOneFileAllowed: 'Only one file can be uploaded at a time!'
+    },
+    system:{
+      menuName: 'Menu Name',
+      enterMenuName: 'Enter Menu Name',
+      status: 'Status',
+      expandCollapse: 'Expand/Collapse',
+      refreshMenuCache: 'Refresh Menu Cache',
+      icon: 'Icon',
+      permissionIdentifier: 'Permission Identifier',
+      enterPermissionIdentifier: 'Enter Permission Identifier',
+      componentPath: 'Component Path',
+      componentName: 'Component Name',
+      parentMenu: 'Parent Menu',
+      menuType: 'Menu Type',
+      menuIcon: 'Menu Icon',
+      routeAddress: 'Route Address',
+      enterRouteAddress: 'Enter Route Address',
+      componentAddress: 'Component Address',
+      routingAccessAddress: 'Access route address, e.g., user. For external addresses, start with http(s)://',
+      controllerAuthCharacter: 'Controller method permission characters, e.g., @PreAuthorize(@ss.hasPermission(\'system:user:list\'))',
+      displayOrder: 'Display Order',
+      menuStatus: 'Menu Status',
+      displayStatus: 'Display Status',
+      selectHidden: 'When hidden, the route will not appear in the sidebar but can still be accessed',
+      show: 'Show',
+      hide: 'Hide',
+      alwaysShow: 'Always Show',
+      selectNotWhen: 'When not selected, if the menu has only one submenu, it will not display itself but will directly show the submenu',
+      cacheStatus: 'Cache Status',
+      selectCache: 'When cache is selected, it will be cached by `keep-alive`, the "Component Name" field must be filled',
+      cache: 'Cache',
+      noCache: 'No Cache',
+      dictionaryName: 'Dictionary Name',
+      enterDictionaryName: 'Enter Dictionary Name',
+      dictionaryType: 'Dictionary Type',
+      enterDictionaryType: 'Enter Dictionary Type',
+      selectDictionaryStatus: 'Select Dictionary Status',
+      dictionaryCode: 'Dictionary Code',
+      dictionaryKey: 'Dictionary Key',
+      dictionarySort: 'Dictionary Sort',
+      colorType: 'Color Type',
+      enterParameterName: 'Enter Parameter Name',
+      enterContent: 'Enter Content',
+      dictionaryLabel: 'Dictionary Label',
+      enterDictionaryLabel: 'Enter Dictionary Label',
+      dataStatus: 'Data Status',
+      dictionaryId: 'Dictionary ID',
+      remark: 'Remark',
+      enterRemark: 'Enter Remark',
+      basePath: 'Base Path',
+      enterBasePath: 'Enter Base Path',
+      hostAddress: 'Host Address',
+      enterHostAddress: 'Enter Host Address',
+      hostPort: 'Host Port',
+      enterHostPort: 'Enter Host Port',
+      username: 'Username',
+      enterUsername: 'Enter Username',
+      password: 'Password',
+      enterPassword: 'Enter Password',
+      activeMode: 'Active Mode',
+      passiveMode: 'Passive Mode',
+      announcementTitle: 'Announcement Title',
+      enterAnnouncementTitle: 'Enter Announcement Title',
+      selectAnnouncementStatus: 'Select Announcement Status',
+      announcementId: 'Announcement ID',
+      announcementType: 'Announcement Type',
+      push: 'Push',
+      pushSelectedNotification: 'Do you want to push the selected notification?',
+      pushSuccess: 'Push Success',
+      announcementContent: 'Announcement Content',
+      selectAnnouncementType: 'Select Announcement Type',
+      selectStatus: 'Select Status',
+      smsConfig: 'SMS Configuration',
+      smsType: 'SMS Type',
+      selectSmsType: 'Select SMS Type',
+      enableStatus: 'Enable Status',
+      selectEnableStatus: 'Select Enable Status',
+      templateCode: 'Template Code',
+      enterTemplateCode: 'Enter Template Code',
+      smsApiTemplateNumber: 'SMS API Template Number',
+      smsChannel: 'SMS Channel',
+      selectSmsChannel: 'Select SMS Channel',
+      smsChannelId: 'SMS Channel ID',
+      selectSmsChannelId: 'Select SMS Channel ID',
+      selectSmsTypeAgain: 'Select SMS Type Again',
+      templateId: 'Template ID',
+      enterTemplateId: 'Enter Template ID',
+      templateName: 'Template Name',
+      enterTemplateName: 'Enter Template Name',
+      templateContent: 'Template Content',
+      enterTemplateContent: 'Enter Template Content',
+      test: 'Test',
+      mobileNumber: 'Mobile Number',
+      enterMobileNumber: 'Enter Mobile Number',
+      parameter: 'Parameter',
+      enter: 'Enter',
+      smsSignature: 'SMS Signature',
+      enterSmsSignature: 'Enter SMS Signature',
+      selectEnableState: 'Select Enable State',
+      serialNumber: 'Serial Number',
+      channelCode: 'Channel Code',
+      enableState: 'Enable State',
+      smsApiKey: 'SMS API Key',
+      smsApiSecret: 'SMS API Secret',
+      smsCallbackUrl: 'SMS Callback URL',
+      emailAccount: 'Email Account',
+      senderName: 'Sender Name',
+      recipientEmail: 'Recipient Email',
+      enterRecipientEmail: 'Enter Recipient Email',
+      emailParameter: 'Email Parameter',
+      emailAddress: 'Email Address',
+      emailPassword: 'Password',
+      smtpServerDomain: 'SMTP Server Domain',
+      smtpServerPort: 'SMTP Server Port',
+      enableSSL: 'Enable SSL',
+      sendingTime: 'Sending Time',
+      receivingEmail: 'Receiving Email',
+      userNumber: 'User Number',
+      userType: 'User Type',
+      emailTitle: 'Email Title',
+      emailContent: 'Email Content',
+      emailParameterAgain: 'Email Parameter',
+      sendingStatus: 'Sending Status',
+      emailAddressAgain: 'Sending Email Address',
+      templateCodeAgain: 'Template Code',
+      templateEncoding: 'Template Encoding',
+      templateSenderName: 'Template Sender Name',
+      messageNumberReturnedBySending: 'Message Number Returned by Sending',
+      sendingException: 'Sending Exception',
+      data: 'data',
+      receivingStatus: 'Receiving Status',
+      receivingTime: 'Receiving Time',
+      smsContent: 'SMS Content',
+      apiSmsNumber: 'API SMS Number',
+      apiSendResult: 'API Send Result',
+      apiRequestNumber: 'API Request Number',
+      apiReceiveStatus: 'API Receive Status',
+      apiReceiveResult: 'API Receive Result'
+    }
+  },
+  home: {
+    "today": "Today",
+    "salesVolume": "Sales Volume",
+    "userVisits": "User Visits",
+    "orderVolume": "Order Volume",
+    "newUsers": "New Users",
+    "userManager": "User Management",
+    "itineraryManager": "Itinerary Management",
+    "guideManager": "Guide Management",
+    "orderManager": "Order Management",
+    "paymentManager": "Payment Management",
+    "refundProcessing": "Refund Processing",
+    "operationalData": "Operational Data",
+    "completedPayments": "Completed Payment Orders",
+    "refundingOrders": "Refunding Orders",
+    "completedTrips": "Completed Trips",
+    "attractions": "Attractions",
+    "itinerary": "Itinerary",
+    "registerGuide": "Register Guide",
+    "memberOverview": "Member Overview",
+    "registeredUsers": "Registered User Count",
+    "sequentialGrowthRate": "Sequential Growth Rate",
+    "visitors": "Visitors",
+    "activeUsers": "Active Users",
+    "placeOrder": "Place Order",
+    "rechargedUsers": "Recharged User Count",
+    "averageOrderValue": "Average Order Value",
+    "completedCustomers": "Completed Customers",
+    "transactionTrend": "Transaction Volume Trend",
+    "30days": "30 Days",
+    "orderAmount": "Order Amount",
+    "orderQuantity": "Order Quantity",
+    "week": "Week",
+    "lastWeekAmount": "Last Week's Amount",
+    "thisWeekAmount": "This Week's Amount",
+    "lastWeekQuantity": "Last Week's Quantity",
+    "thisWeekQuantity": "This Week's Quantity",
+    "month": "Month",
+    "lastMonthAmount": "Last Month's Amount",
+    "thisMonthAmount": "This Month's Amount",
+    "lastMonthQuantity": "Last Month's Quantity",
+    "thisMonthQuantity": "This Month's Quantity",
+    "year": "Year",
+    "lastYearAmount": "Last Year's Amount",
+    "thisYearAmount": "This Year's Amount",
+    "lastYearQuantity": "Last Year's Quantity",
+    "thisYearQuantity": "This Year's Quantity",
+    "yesterdayData": "yesterday Data",
+    "quickAccess": "quickAccess",
+    "yesterday": "Yesterday",
+    "last7Days": "Last 7 Days",
+    "last30Days": "Last 30 Days",
+    "startDate": "Start Date",
+    "endDate": "End Date"
+  },
+  aftersale:{
+    "guideName": "Guide Name",
+    "enterGuideName": "Please enter guide name",
+    "refundNumber": "Refund Number",
+    "enterRefundNumber": "Please enter refund number",
+    "orderNumber": "Order Number",
+    "enterOrderNumber": "Please enter order number",
+    "afterSalesStatus": "After Sales Status",
+    "selectAfterSalesStatus": "Please select after sales status",
+    "all": "All",
+    "afterSalesMethod": "After Sales Method",
+    "selectAfterSalesMethod": "Please select after sales method",
+    "creationTime": "Creation Time",
+    "startDate": "Start Date",
+    "endDate": "End Date",
+    "appointmentInfo": "Appointment Information",
+    "orderAmount": "Order Amount",
+    "yen": "Yen",
+    "buyer": "Buyer",
+    "applicationTime": "Application Time",
+    "processRefund": "Process Refund",
+    "orderInfo": "Order Information",
+    "orderNo": "Order Number: ",
+    "orderType": "Order Type: ",
+    "buyerMessage": "Buyer Message: ",
+    "orderSource": "Order Source: ",
+    "contactPhone": "Contact Phone: ",
+    "merchantRemark": "Merchant Remark: ",
+    "paymentNumber": "Payment Number: ",
+    "paymentMethod": "Payment Method: ",
+    "afterSalesInfo": "After Sales Information",
+    "refundNumberLabel": "Refund Number: ",
+    "afterSalesType": "After Sales Type: ",
+    "refundAmount": "Refund Amount: ",
+    "refundReason": "Refund Reason: ",
+    "additionalDescription": "Additional Description: ",
+    "voucherImage": "Voucher Image: ",
+    "refundStatus": "Refund Status",
+    "agreeRefund": "Agree to Refund",
+    "denyRefund": "Deny Refund",
+    "confirmRefund": "Confirm Refund",
+    "reminder": "Reminder: ",
+    "guideDateReminder": "If the guide date has not arrived, please click to agree to refund the buyer.",
+    "expiredReminder": "If it has expired, please actively contact the buyer.",
+    "reserveGuideInfo": "Reserve Guide Information",
+    "reservationInfo": "Reservation Information",
+    "reservationPrice": "Reservation Price",
+    "afterSalesLog": "After Sales Log",
+    "system": "System",
+    "denyAfterSales": "Deny After Sales",
+    "approvalRemark": "Approval Remark",
+    "enterApprovalRemark": "Please enter approval remark",
+    "afterSalesOrderNotFound": "After Sales Order Not Found",
+    "agreeRefundPrompt": "Do you agree to the refund?",
+    "confirmRefundPrompt": "Do you confirm the refund?"
+  },
+  statistics:{
+    "guide_rank": "Guide Ranking",
+    "guide_id": "Guide ID",
+    "guide_image": "Guide Image",
+    "guide_nickname": "Guide Nickname",
+    "views": "Views",
+    "visitors": "Visitors",
+    "added_to_cart": "Items Added to Cart",
+    "ordered_items": "Ordered Items",
+    "paid_items": "Paid Items",
+    "payment_amount": "Payment Amount",
+    "refund_items": "Refund Items",
+    "refund_amounts": "Refund Amount",
+    "favorites": "Favorites",
+    "conversion_rate": "Visitor to Payment Conversion Rate (%)",
+    "guide_overview": "Guide Overview",
+    "guide_page_views": "Total page views for all guide/itinerary details under selected conditions. Multiple visits by the same person within the statistical time frame are counted multiple times.",
+    "guide_visitors": "Number of visitors to any guide/itinerary details under selected conditions. Multiple visits by the same person within the statistical time frame are counted as one.",
+    "paid_items_count": "Total number of items in successfully paid orders under selected conditions.",
+    "payment_total": "Total amount of successful payment orders under selected conditions.",
+    "refund_count": "Total number of successfully refunded items under selected conditions.",
+    "refund_amount": "Total amount of successful refunds under selected conditions.",
+    "quantity": "Quantity",
+    "amount": "Amount",
+    "guide_status_excel": "Guide Status.xls",
+    "attraction_rank": "Attraction Ranking",
+    "attraction_id": "Attraction ID",
+    "attraction_image": "Attraction Image",
+    "attraction_name": "Attraction Name",
+    "attraction_overview": "Attraction Overview",
+    "attraction_page_views": "Total page views for all attraction details under selected conditions. Multiple visits by the same person within the statistical time frame are counted multiple times.",
+    "attraction_visitors": "Number of visitors to any attraction details under selected conditions. Multiple visits by the same person within the statistical time frame are counted as one.",
+    "itinerary_rank": "Itinerary Ranking",
+    "itinerary_id": "Itinerary ID",
+    "itinerary_overview": "Itinerary Overview",
+    "itinerary_page_views": "Total page views for all itinerary details under selected conditions. Multiple visits by the same person within the statistical time frame are counted multiple times.",
+    "itinerary_visitors": "Number of visitors to any itinerary details under selected conditions. Multiple visits by the same person within the statistical time frame are counted as one.",
+    "itinerary_status_excel": "Itinerary Status.xls",
+    "attraction_status_excel": "Attraction Status.xls",
+    "total_members": "Total Members",
+    "total_spent": "Total Spent",
+    "member_terminal": "Member Terminal",
+    "yesterday_order_count": "Yesterday's Order Count",
+    "this_month_order_count": "This Month's Order Count",
+    "yesterday_payment": "Yesterday's Payment",
+    "this_month_payment": "This Month's Payment",
+    "transaction_status": "Transaction Status",
+    "revenue": "Revenue",
+    "guide_booking_payment": "Guide Booking Payment",
+    "actual_payment": "Actual Payment for Purchased Products",
+    "total_refund": "Total Refund Amount",
+    "sales_revenue": "Sales Revenue",
+    "payment_received": "Payment Received",
+    "recharge_amount": "Recharge Amount",
+    "expense_amount": "Expense Amount"
+  },
 }

+ 1136 - 0
src/locales/ja.ts

@@ -0,0 +1,1136 @@
+export default {
+  common: {
+    systemName: '旅行者とツアーガイドのマッチングシステム',
+    travelerDetail: '旅行者詳細情報',
+    inputText: '入力してください',
+    selectText: '選択してください',
+    startTimeText: '開始時間',
+    endTimeText: '終了時間',
+    login: 'ログイン',
+    required: '必須項目です',
+    loginOut: 'ログアウト',
+    document: 'プロジェクトドキュメント',
+    profile: '個人センター',
+    reminder: 'ヒント',
+    loginOutMessage: '本システムをログアウトしますか?',
+    back: '戻る',
+    ok: '確定',
+    save: '保存',
+    cancel: 'キャンセル',
+    close: '閉じる',
+    reload: '再読み込み',
+    success: '成功',
+    closeTab: 'タブを閉じる',
+    closeTheLeftTab: '左側のタブを閉じる',
+    closeTheRightTab: '右側のタブを閉じる',
+    closeOther: '他のタブを閉じる',
+    closeAll: '全てのタブを閉じる',
+    prevLabel: '前へ',
+    nextLabel: '次へ',
+    skipLabel: 'スキップ',
+    doneLabel: '終了',
+    menu: 'メニュー',
+    menuDes: 'ルート構造でレンダリングされたメニューバー',
+    collapse: '展開・折りたたみ',
+    collapseDes: 'メニューバーの展開と折りたたみ',
+    tagsView: 'タブビュー',
+    tagsViewDes: 'ルート履歴を記録する',
+    tool: 'ツール',
+    toolDes: 'カスタマイズシステムの設定',
+    query: '検索',
+    reset: 'リセット',
+    shrink: '折りたたむ',
+    expand: '展開する',
+    confirmTitle: 'システムヒント',
+    exportMessage: 'データ項目をエクスポートしますか?',
+    importMessage: 'データ項目をインポートしますか?',
+    createSuccess: '新規作成に成功',
+    updateSuccess: '更新に成功',
+    delMessage: '選択したデータを削除しますか?',
+    delDataMessage: 'データを削除しますか?',
+    delNoData: '削除するデータを選択してください',
+    delSuccess: '削除に成功',
+    index: '番号',
+    status: 'ステータス',
+    createTime: '作成時間',
+    updateTime: '更新時間',
+    copy: 'コピー',
+    copySuccess: 'コピー成功',
+    copyError: 'コピー失敗'
+  },
+  lock: {
+    lockScreen: '画面をロック',
+    lock: 'ロック',
+    lockPassword: 'パスワード',
+    unlock: 'クリックして解除',
+    backToLogin: 'ログインに戻る',
+    entrySystem: 'システムに入る',
+    placeholder: 'ロック画面のパスワードを入力してください',
+    message: 'ロック画面パスワードが間違っています'
+  },
+  error: {
+    noPermission: `申し訳ありませんが、このページへのアクセス権限がありません。`,
+    pageError: '申し訳ありませんが、アクセスしたページは存在しません。',
+    networkError: '申し訳ありませんが、サーバーからエラーが報告されました。',
+    returnToHome: 'ホームに戻る'
+  },
+  permission: {
+    hasPermission: `操作権限タグの値を設定してください`,
+    hasRole: `ロール権限タグの値を設定してください`
+  },
+  setting: {
+    projectSetting: 'プロジェクト設定',
+    theme: 'テーマ',
+    layout: 'レイアウト',
+    systemTheme: 'システムテーマ',
+    menuTheme: 'メニューテーマ',
+    interfaceDisplay: 'インターフェース表示',
+    breadcrumb: 'パンくずリスト',
+    breadcrumbIcon: 'パンくずアイコン',
+    collapseMenu: 'メニューを折りたたむ',
+    hamburgerIcon: 'ハンバーガーアイコン',
+    screenfullIcon: '全画面アイコン',
+    sizeIcon: 'サイズアイコン',
+    localeIcon: '多言語アイコン',
+    messageIcon: 'メッセージアイコン',
+    tagsView: 'タグビュー',
+    logo: 'ロゴ',
+    greyMode: 'グレーモード',
+    fixedHeader: 'ヘッダーを固定',
+    headerTheme: 'ヘッダーテーマ',
+    cutMenu: 'メニューをカット',
+    copy: 'コピー',
+    clearAndReset: 'キャッシュをクリアしてリセット',
+    copySuccess: 'コピー成功',
+    copyFailed: 'コピー失敗',
+    footer: 'フッター',
+    uniqueOpened: 'メニューのアコーディオン',
+    tagsViewIcon: 'タグビューアイコン',
+    reExperienced: 'ログアウトして再体験してください',
+    fixedMenu: 'メニューを固定'
+  },
+  size: {
+    default: 'デフォルト',
+    large: '大',
+    small: '小'
+  },
+  login: {
+    welcome: 'ようこそ!',
+    message: '即戦力の中後台管理システム',
+    tenantname: 'テナント名',
+    username: 'ユーザー名',
+    password: 'パスワード',
+    code: '認証コード',
+    login: 'ログイン',
+    relogin: '再ログイン',
+    otherLogin: 'その他のログイン方法',
+    register: '登録',
+    checkPassword: 'パスワードの確認',
+    remember: '記憶する',
+    hasUser: 'すでにアカウントをお持ちですか? ログインする',
+    forgetPassword: 'パスワードを忘れた方はこちら',
+    tenantNamePlaceholder: 'テナント名を入力してください',
+    usernamePlaceholder: 'ユーザー名を入力してください',
+    passwordPlaceholder: 'パスワードを入力してください',
+    codePlaceholder: '認証コードを入力してください',
+    mobileTitle: '携帯電話でログイン',
+    mobileNumber: '携帯電話番号',
+    mobileNumberPlaceholder: '携帯電話番号を入力してください',
+    backLogin: '戻る',
+    getSmsCode: '認証コードを取得する',
+    btnMobile: '携帯電話でログイン',
+    btnQRCode: 'QRコードでログイン',
+    qrcode: 'QRコードをスキャンしてログイン',
+    btnRegister: '登録',
+    SmsSendMsg: '認証コードが送信されました'
+  },
+  captcha: {
+    verification: 'セキュリティ検証を完了してください',
+    slide: '右にスライドして検証を完了してください',
+    point: '順番にクリックしてください',
+    success: '検証成功',
+    fail: '検証失敗'
+  },
+  router: {
+    login: 'ログイン',
+    socialLogin: 'ソーシャルログイン',
+    home: 'ホームページ',
+    analysis: '分析ページ',
+    workplace: 'ワークプレイス'
+  },
+  analysis: {
+    newUser: '新規ユーザー',
+    unreadInformation: '未読メッセージ',
+    transactionAmount: '取引金額',
+    totalShopping: '総購入量',
+    monthlySales: '月間売上',
+    userAccessSource: 'ユーザーアクセス元',
+    january: '1月',
+    february: '2月',
+    march: '3月',
+    april: '4月',
+    may: '5月',
+    june: '6月',
+    july: '7月',
+    august: '8月',
+    september: '9月',
+    october: '10月',
+    november: '11月',
+    december: '12月',
+    estimate: '推定',
+    actual: '実際',
+    directAccess: 'ダイレクトアクセス',
+    mailMarketing: 'メールマーケティング',
+    allianceAdvertising: 'アライアンス広告',
+    videoAdvertising: 'ビデオ広告',
+    searchEngines: '検索エンジン',
+    weeklyUserActivity: '週間ユーザーアクティビティ',
+    activeQuantity: '活動量',
+    monday: '月曜',
+    tuesday: '火曜',
+    wednesday: '水曜',
+    thursday: '木曜',
+    friday: '金曜',
+    saturday: '土曜',
+    sunday: '日曜'
+  },
+  workplace: {
+    welcome: 'こんにちは',
+    happyDay: '毎日が楽しい日でありますように!',
+    toady: '今日の天気は晴れ',
+    notice: 'お知らせ',
+    project: 'プロジェクト数',
+    access: 'プロジェクトアクセス',
+    toDo: 'やるべきこと',
+    introduction: '真面目な紹介',
+    shortcutOperation: 'ショートカット',
+    operation: '操作',
+    index: '指数',
+    personal: '個人',
+    team: 'チーム',
+    quote: '引用',
+    contribution: '貢献',
+    hot: '人気',
+    yield: '生産量',
+    dynamic: 'ダイナミック',
+    push: 'プッシュ',
+    follow: 'フォロー'
+  },
+  form: {
+    input: '入力フィールド',
+    inputNumber: '数値入力フィールド',
+    default: 'デフォルト',
+    icon: 'アイコン',
+    mixed: '複合タイプ',
+    textarea: 'テキストエリア',
+    slot: 'スロット',
+    position: '位置',
+    autocomplete: '自動補完',
+    select: 'セレクタ',
+    selectGroup: '選択グループ',
+    selectV2: '仮想リストセレクタ',
+    cascader: 'カスケーダー',
+    switch: 'スイッチ',
+    rate: '評価',
+    colorPicker: 'カラーピッカー',
+    transfer: 'トランスファーボックス',
+    render: 'レンダラー',
+    radio: 'ラジオボタン',
+    button: 'ボタン',
+    checkbox: 'チェックボックス',
+    slider: 'スライダー',
+    datePicker: '日付セレクタ',
+    shortcuts: 'ショートカット',
+    today: '今日',
+    yesterday: '昨日',
+    aWeekAgo: '一週間前',
+    week: '週',
+    year: '年',
+    month: '月',
+    dates: '日付',
+    daterange: '日付範囲',
+    monthrange: '月範囲',
+    dateTimePicker: '日時セレクタ',
+    dateTimerange: '日時範囲',
+    timePicker: '時間セレクタ',
+    timeSelect: '時間選択',
+    inputPassword: 'パスワード入力フィールド',
+    passwordStrength: 'パスワード強度',
+    operate: '操作',
+    change: '変更',
+    restore: '復元',
+    disabled: '無効',
+    disablement: '無効解除',
+    delete: '削除',
+    add: '追加',
+    setValue: '値を設定',
+    resetValue: '値をリセット',
+    set: '設定',
+    subitem: 'サブアイテム',
+    formValidation: 'フォーム検証',
+    verifyReset: '検証リセット',
+    remark: '備考'
+  },
+  watermark: {
+    watermark: 'ウォーターマーク'
+  },
+  table: {
+    table: 'テーブル',
+    index: 'インデックス',
+    title: 'タイトル',
+    author: '著者',
+    createTime: '作成時間',
+    action: 'アクション',
+    pagination: 'ページネーション',
+    reserveIndex: 'インデックスを重ねる',
+    restoreIndex: 'インデックスを復元',
+    showSelections: '複数選択を表示',
+    hiddenSelections: '複数選択を非表示',
+    showExpandedRows: '拡張行を表示',
+    hiddenExpandedRows: '拡張行を非表示',
+    header: 'ヘッダー'
+  },
+  action: {
+    create: '新規作成',
+    add: '追加',
+    del: '削除',
+    delete: '削除',
+    edit: '編集',
+    update: '更新',
+    preview: 'プレビュー',
+    more: 'もっと',
+    sync: '同期',
+    save: '保存',
+    detail: '詳細',
+    export: 'エクスポート',
+    import: 'インポート',
+    generate: '生成',
+    logout: '強制ログアウト',
+    test: 'テスト',
+    typeCreate: '辞書タイプの新規作成',
+    typeUpdate: '辞書タイプの編集',
+    dataCreate: '辞書データの新規作成',
+    dataUpdate: '辞書データの編集'
+  },
+  dialog: {
+    dialog: 'ダイアログ',
+    open: '開く',
+    close: '閉じる'
+  },
+  sys: {
+    api: {
+      operationFailed: '操作に失敗しました',
+      errorTip: 'エラーヒント',
+      errorMessage: '操作に失敗しました、システムエラーです!',
+      timeoutMessage: 'ログインがタイムアウトしました、再度ログインしてください!',
+      apiTimeoutMessage:
+        'APIリクエストがタイムアウトしました、ページをリフレッシュして再試行してください!',
+      apiRequestFailed: 'リクエストエラーです、しばらくしてから再試行してください',
+      networkException: 'ネットワーク異常',
+      networkExceptionMsg: 'ネットワーク異常です、ネットワーク接続が正常かどうか確認してください!',
+      errMsg401: 'ユーザーに権限がありません(トークン、ユーザー名、パスワードが間違っています)!',
+      errMsg403: 'ユーザーには認可されていますが、アクセスは禁止されています。',
+      errMsg404: 'ネットワークリクエストエラー、リソースが見つかりませんでした!',
+      errMsg405: 'ネットワークリクエストエラー、許可されていないメソッドです!',
+      errMsg408: 'ネットワークリクエストのタイムアウト!',
+      errMsg500: 'サーバーエラーです、管理者に連絡してください!',
+      errMsg501: 'ネットワークが実装されていません!',
+      errMsg502: 'ネットワークエラー!',
+      errMsg503: 'サービス利用不可、サーバーが一時的に過負荷またはメンテナンス中です!',
+      errMsg504: 'ネットワークタイムアウト!',
+      errMsg505: 'リクエストのHTTPバージョンがサポートされていません!',
+      errMsg901: 'デモモードです、書き込み操作はできません!'
+    },
+    app: {
+      logoutTip: '温かいヒント',
+      logoutMessage: 'システムをログアウトしますか?',
+      menuLoading: 'メニューをロード中…'
+    },
+    exception: {
+      backLogin: 'ログインに戻る',
+      backHome: 'ホームに戻る',
+      subTitle403: '申し訳ありませんが、このページへのアクセス権限がありません。',
+      subTitle404: '申し訳ありませんが、アクセスしたページは存在しません。',
+      subTitle500: '申し訳ありませんが、サーバーからエラーが報告されました。',
+      noDataTitle: '現在のページにデータがありません',
+      networkErrorTitle: 'ネットワークエラー',
+      networkErrorSubTitle:
+        '申し訳ありませんが、ネットワーク接続が切断されました。ネットワークを確認してください!'
+    },
+    lock: {
+      unlock: 'クリックして解除',
+      alert: 'ロック画面パスワードが間違っています',
+      backToLogin: 'ログインに戻る',
+      entry: 'システムに入る',
+      placeholder: 'ロック画面パスワードまたはユーザーパスワードを入力してください'
+    },
+    login: {
+      backSignIn: '戻る',
+      signInFormTitle: 'ログイン',
+      ssoFormTitle: 'サードパーティ認証',
+      mobileSignInFormTitle: '携帯電話でログイン',
+      qrSignInFormTitle: 'QRコードでログイン',
+      signUpFormTitle: '登録',
+      forgetFormTitle: 'パスワードリセット',
+      signInTitle: '即戦力の中後台管理システム',
+      signInDesc: '個人情報を入力して使用を開始してください!',
+      policy: 'xxxのプライバシーポリシーに同意します',
+      scanSign: '「確認」をクリックした後、スキャンしてログインを完了してください',
+      loginButton: 'ログイン',
+      registerButton: '登録',
+      rememberMe: '記憶する',
+      forgetPassword: 'パスワードを忘れましたか?',
+      otherSignIn: '他のログイン方法',
+      loginSuccessTitle: 'ログイン成功',
+      loginSuccessDesc: 'お帰りなさい',
+      accountPlaceholder: 'アカウントを入力してください',
+      passwordPlaceholder: 'パスワードを入力してください',
+      smsPlaceholder: '認証コードを入力してください',
+      mobilePlaceholder: '携帯電話番号を入力してください',
+      policyPlaceholder: '登録するにはチェックしてください',
+      diffPwd: 'パスワードが一致しません',
+      userName: 'アカウント',
+      password: 'パスワード',
+      confirmPassword: 'パスワードを確認',
+      email: 'メール',
+      smsCode: 'SMS認証コード',
+      mobile: '携帯電話番号'
+    }
+  },
+  profile: {
+    user: {
+      title: '個人情報',
+      username: 'ユーザー名',
+      nickname: 'ニックネーム',
+      mobile: '携帯電話番号',
+      email: 'ユーザーメール',
+      dept: '所属部門',
+      posts: '所属職',
+      roles: '所属ロール',
+      sex: '性別',
+      man: '男性',
+      woman: '女性',
+      createTime: '作成日'
+    },
+    info: {
+      title: 'ガイド関連情報',
+      basicInfo: '基本情報',
+      resetPwd: 'パスワードの変更',
+      userSocial: 'ソーシャル情報',
+      commentInfo: 'コメント情報',
+      Schedule: 'スケジュール情報',
+      languageUsed: '使用言語',
+      textAndImageIntro: '自己紹介',
+      expertiseField: '得意分野',
+      serviceItems: 'サービス項目',
+      precautions: '注意事項',
+      servicePrice: 'サービス料金',
+      maxParticipants: '対象者最大人数',
+      region: '地域',
+      serviceStartDate: 'サービス開始日',
+      serviceEndDate: 'サービス終了日',
+      settlementBankInfo: '決済銀行情報',
+      ratingStars: '総合',
+      expertiseStars: '専門知識',
+      serviceStars: 'サービス',
+      reviewContent: '評価内容',
+      likesCount: 'いいね数',
+      work: '仕事',
+      rest: '休憩',
+      booked: '予約済み',
+      modifyDialog1: '今日以前の日付は変更できません',
+      modifyDialog2: 'この日付は予約済みで、変更できません。'
+    },
+    rules: {
+      nickname: 'ニックネームを入力してください',
+      mail: 'メールアドレスを入力してください',
+      truemail: '正しいメールアドレスを入力してください',
+      phone: '正しい携帯電話番号を入力してください',
+      truephone: '正しい携帯電話番号を入力してください'
+    },
+    password: {
+      oldPassword: '旧パスワード',
+      newPassword: '新パスワード',
+      confirmPassword: 'パスワードの確認',
+      oldPwdMsg: '旧パスワードを入力してください',
+      newPwdMsg: '新パスワードを入力してください',
+      cfPwdMsg: 'パスワードの確認を入力してください',
+      pwdRules: '6〜20文字の長さ',
+      diffPwd: 'パスワードが一致しません'
+    }
+  },
+  cropper: {
+    selectImage: '画像を選択',
+    uploadSuccess: 'アップロードに成功しました',
+    modalTitle: 'アバターアップロード',
+    okText: '確認してアップロード',
+    btn_reset: 'リセット',
+    btn_rotate_left: '反時計回りに回転',
+    btn_rotate_right: '時計回りに回転',
+    btn_scale_x: '水平に反転',
+    btn_scale_y: '垂直に反転',
+    btn_zoom_in: '拡大',
+    btn_zoom_out: '縮小',
+    preview: 'プレビュー'
+  },
+  'OAuth 2.0': 'OAuth 2.0', // メニュー名が OAuth 2.0 の場合、常に警告が表示されないようにする
+  guide: {
+    common: {
+      search: '検索',
+      reset: 'リセット',
+      add: '追加',
+      export: 'エクスポート',
+      expandCollapse: '展開/折りたたみ',
+      actions: '操作',
+      edit: '編集',
+      delete: '削除',
+      confirm: '確定',
+      cancel: 'キャンセル',
+    },
+    category: {
+      chineseName: '中国語名',
+      enterChineseName: '中国語名を入力してください',
+      japaneseName: '日本語名',
+      enterJapaneseName: '日本語名を入力してください',
+      englishName: '英語名',
+      enterEnglishName: '英語名を入力してください',
+      creationTime: '作成時間',
+      startDate: '開始日',
+      endDate: '終了日',
+      parentCategoryID: '親カテゴリID',
+      categoryID: 'カテゴリID',
+      mobileCategoryImage: 'モバイルカテゴリ画像',
+      categorySort: 'カテゴリ順序',
+      enableStatus: '有効状態',
+      selectParentCategoryID: '親カテゴリIDを選択してください',
+      enterCategorySort: 'カテゴリ順序を入力してください',
+      topTouristAttractionCategory: 'トップ観光地カテゴリ'
+    },
+    sight:{
+      id: '番号',
+      baseInfo: '基本情報',
+      attractionCategory: 'カテゴリ',
+      region: '地域',
+      attractionInfo: '観光地情報',
+      phoneNumber: '電話番号',
+      openingHours: '開放時間',
+      enterOpeningHours: '開放時間を入力してください',
+      multilingualExpansionInfo: '多言語拡張情報',
+      comments: 'コメント',
+      attractionCoverImage: 'カバー画像',
+      enterPhoneNumber: '電話番号を入力してください',
+      attractionCarouselImages: 'カルーセル画像、最大6枚',
+      officialWebsiteURL: '公式ウェブサイト',
+      enterOfficialWebsiteURL: '公式ウェブサイトのURLを入力してください',
+      playTime: '遊び時間',
+      enterApproximatePlayTime: 'おおよその遊び時間を入力してください',
+      sort: '並び替え',
+      selectATouristAttraction: '観光地を選択してください',
+      selectAttractionCategory: '観光地カテゴリを選択してください',
+      ratingStars: '総合評価の星',
+      sceneryStars: '景色の星',
+      serviceStars: 'サービスの星',
+      reviewContent: 'レビュー内容',
+      tourDuration: 'ツアーの長さ',
+      likesCount: 'いいね数',
+      enterLikesCount: 'いいね数を入力してください',
+      image: '画像',
+      multilingualType: '多言語タイプ',
+      title: 'タイトル',
+      introduction: '紹介',
+      ticketsAndReservations: 'チケットと予約',
+      keywordGroup: 'キーワード',
+      enterKeywords: 'キーワードを入力してください',
+      subtitle: '副題',
+      attractionTextualDescription: '観光地説明',
+      address: '住所',
+      additionalInfo: '追加情報'
+    },
+    trip:{
+      guideId: 'ガイド番号',
+      enterGuideId: 'ガイド番号を入力してください',
+      creationTime: '作成時間',
+      startDate: '開始日',
+      endDate: '終了日',
+      id: '番号',
+      guide: 'ガイド',
+      coverImage: 'カバー画像',
+      maxParticipants: '最大人数',
+      guidePrice: 'ガイド料金',
+      multilingualExpansionInfo: '多言語拡張情報',
+      includedAttractions: '含まれる観光地',
+      startTime: '開始時刻',
+      selectJourneyStartTime: '旅行の開始時刻を選択',
+      endTime: '終了時刻',
+      selectJourneyEndTime: '旅行の終了時刻を選択',
+      guideServicePrice: 'ガイドサービス料金',
+      enterGuideServicePrice: 'ガイドサービス料金を入力してください',
+      enableStatus: '有効状態',
+      importAttractions: '観光地をインポート',
+      attractionCategory: '観光地カテゴリ',
+      region: '地域',
+      attractionInfo: '観光地情報',
+      openingHours: '開放時間',
+      sightseeingSpotId: '観光地番号',
+      journeyStartTime: '旅行開始時間',
+      requiredTime: '必要時間',
+      selectJourneyBasicInfo: '旅行基本情報を選択してください',
+      multilingualType: '多言語タイプ',
+      multilingualTitle: 'タイトル',
+      includedItems: '含まれる項目',
+      excludedItems: '含まれない項目',
+      featureTextualDescription: '特徴説明',
+      additionalInfo: '追加情報'
+    },
+    user:{
+      userAccount: 'アカウント',
+      enterUserAccount: 'アカウントを入力してください',
+      mobileNumber: '携帯番号',
+      enterMobileNumber: '携帯番号を入力してください',
+      guideLanguage: '言語',
+      selectGuideLanguage: 'ガイド言語を選択してください',
+      creationTime: '作成時間',
+      startDate: '開始日',
+      endDate: '終了日',
+      userId: 'ID',
+      userNickname: 'ニックネーム',
+      lastLoginTime: '最終ログイン時間',
+      expertiseField: '得意分野',
+      guideReviews: 'ガイドのレビュー',
+      userPassword: 'パスワード',
+      enterUserPassword: 'ユーザーパスワードを入力してください',
+      enterUserNickname: 'ユーザーのニックネームを入力してください',
+      userEmail: 'メール',
+      enterUserEmail: 'ユーザーメールを入力してください',
+      userGender: '性別',
+      pleaseSelect: '選択してください',
+      selectGuideUsageLanguage: 'ガイドの使用言語を選択してください',
+      textAndImageIntro: '自己紹介',
+      serviceItems: 'サービス項目',
+      enterServiceItems: 'サービス項目を入力してください',
+      precautions: '注意事項',
+      enterServicePrecautions: 'サービス時の注意事項を入力してください',
+      guideServicePrice: 'サービス料金',
+      maxNumberOfPeople: '最大人数',
+      serviceTargetMaxNumber: 'サービス対象最大人数',
+      region: '地域',
+      guideServiceStartDate: 'サービス開始日',
+      selectGuideServiceStartDate: 'ガイドサービス開始日を選択してください',
+      guideServiceEndDate: 'サービス終了日',
+      selectGuideServiceEndDate: 'ガイドサービス終了日を選択してください',
+      bankInformation: '銀行情報',
+      enterBankInformation: '銀行情報を入力してください',
+      enterCorrectEmail: '正しいメールアドレスを入力してください',
+      enterCorrectMobileNumber: '正しい携帯番号を入力してください',
+      id: '番号',
+      reviewer: 'レビュアー',
+      type: 'タイプ',
+      ratingStars: '総合評価',
+      expertiseStars: '専門知識の星',
+      serviceStars: 'サービスの星',
+      reviewTime: 'レビュー時間',
+      reviewerId: 'レビュアーID',
+      enterReviewerUserId: 'レビュアーのユーザーIDを入力してください',
+      enterReviewerName: 'レビュアーの名前を入力してください',
+      selectCustomerReviewSelfReview: '顧客レビュー/自己レビューを選択してください',
+      reviewContent: 'レビュー内容',
+      likesCount: 'いいね数',
+      registrationTime: '登録時間',
+      loginTime: 'ログイン時間',
+      avatar: 'アバター',
+      status: 'ステータス',
+      realName: '本名',
+      enterRealName: '本名を入力してください',
+      birthDate: '生年月日',
+      selectBirthDate: '生年月日を選択してください',
+      basicInformation: '基本情報',
+      accountDetails: 'アカウント詳細',
+      orderManagement: '注文管理',
+      afterSalesManagement: 'アフターケア管理',
+      favoritesRecord: 'お気に入り記録',
+      username: 'ユーザー名',
+      location: '所在地',
+      registrationIP: '登録IP',
+    },
+    order:{
+      merchantOrderNumber: '注文番号',
+      enterMerchantOrderNumber: '注文番号を入力してください',
+      paymentOrderNumber: '支払い番号',
+      enterPaymentOrderNumber: '支払い番号を入力してください',
+      paymentStatus: '支払い状態',
+      selectPaymentStatus: '支払い状態を選択してください',
+      creationTime: '作成時間',
+      startDate: '開始日',
+      endDate: '終了日',
+      id: '番号',
+      paymentAmount: '支払い金額',
+      refundAmount: '返金額',
+      handlingFee: '手数料',
+      orderNumber: '注文番号',
+      merchant: '注文ID',
+      payment: '支払い番号',
+      channel: 'Stripe支払No',
+      paymentTime: '支払い時間',
+      productTitle: '商品タイトル',
+      orderDetails: '注文詳細',
+      handlingFeeRate: '手数料率',
+      expirationTime: '失効時間',
+      updateTime: '更新時間',
+      successTime: '成功時間',
+      productDescription: '商品説明',
+      paymentIP: '支払いIP',
+      notificationURL: '通知URL',
+      asyncCallbackContent: '支払いチャネルの非同期コールバック内容',
+      refundOrderNumber: '返金注文番号',
+      enterRefundOrderNumber: '返金注文番号を入力してください',
+      refundStatus: '返金状態',
+      selectRefundStatus: '返金状態を選択してください',
+      details: '詳細',
+      refundTime: '返金時間',
+      refundReason: '返金理由',
+      refundIP: '返金IP',
+      channelErrorCode: 'チャネルエラーコード',
+      orderStatus: '注文状態',
+      all: '全て',
+      customTime: 'カスタム時間',
+      aggregateSearch: '集約検索',
+      enter: '入力してください',
+      remark: '備考',
+      userUID: 'ユーザーUID',
+      userNickname: 'ニックネーム',
+      userPhone: '電話',
+      merchantRemark: 'ガイド・システム備考',
+      enterOrderRemark: '注文備考を入力してください',
+      reservationInfo: '予約情報',
+      reservationUnitPrice: '予約単価/ガイド日',
+      afterSaleStatus: 'アフターケア',
+      orderPrice: '注文価格',
+      tourist: '観光客',
+      orderTime: '注文時間',
+      paymentMethod: '支払方法',
+      yen: '円',
+      actualPayment: '実際の支払い',
+      orderInfo: '注文情報',
+      buyer: '買い手',
+      orderType: '注文タイプ',
+      orderSource: '注文ソース',
+      buyerMessage: '観光客のメッセージ',
+      reservationGuideInfo: '予約ガイド情報',
+      totalAmountOfGoods: '商品総額',
+      totalPayment: '総支払額',
+      visitorInfo: '観光客情報',
+      orderOperationLog: '注文操作ログ',
+      notpay: '未支払',
+      reservationdate: '予約日'
+    },
+    infra:{
+      configName: '設定名',
+      enterConfigName: '設定名を入力してください',
+      storage: 'ストレージ',
+      selectStorage: 'ストレージを選択してください',
+      creationTime: '作成時間',
+      startDate: '開始日',
+      endDate: '終了日',
+      id: '番号',
+      remark: '備考',
+      mainConfig: 'メイン設定',
+      enterRemark: '備考を入力してください',
+      basePath: '基本パス',
+      enterBasePath: '基本パスを入力してください',
+      hostAddress: 'ホストアドレス',
+      enterHostAddress: 'ホストアドレスを入力してください',
+      hostPort: 'ホストポート',
+      enterHostPort: 'ホストポートを入力してください',
+      username: 'ユーザー名',
+      enterUsername: 'ユーザー名を入力してください',
+      enterPassword: 'パスワードを入力してください',
+      password: 'パスワード',
+      activeMode: 'アクティブモード',
+      passiveMode: 'パッシブモード',
+      nodeAddress: 'ノードアドレス',
+      enterNodeAddress: 'ノードアドレスを入力してください',
+      storageBucket: 'ストレージバケット',
+      enterBucket: 'バケットを入力してください',
+      enterAccessKey: 'アクセスキーを入力してください',
+      enterAccessSecret: 'アクセスシークレットを入力してください',
+      customDomain: 'カスタムドメイン',
+      filePath: 'ファイルパス',
+      enterFilePath: 'ファイルパスを入力してください',
+      fileType: 'ファイルタイプ',
+      enterFileType: 'ファイルタイプを入力してください',
+      uploadFile: 'アップロード',
+      fileSize: 'ファイルサイズ',
+      fileName: 'ファイル名',
+      fileContent: 'ファイル内容',
+      preview: 'プレビュー',
+      download: 'ダウンロード',
+      uploadTime: 'アップロード時間',
+      userId: 'ユーザーID',
+      enterUserId: 'ユーザーIDを入力してください',
+      executionDuration: '実行時間',
+      requestTime: 'リクエスト時間',
+      enterExecutionDuration: '実行時間を入力してください',
+      resultCode: '結果コード',
+      enterResultCode: '結果コードを入力してください',
+      logId: 'ログID',
+      requestMethod: 'リクエスト方法',
+      requestAddress: 'リクエストアドレス',
+      operationResult: '操作結果',
+      success: '成功',
+      failure: '失敗',
+      operationModule: '操作モジュール',
+      operationName: '操作名',
+      operationType: '操作タイプ',
+      operation: '操作',
+      logPrimaryKey: 'ログ主キー',
+      linkTracing: 'リンクトレーシング',
+      userIP: 'ユーザーIP',
+      userInfo: 'ユーザー情報',
+      userUA: 'ユーザーUA',
+      requestInfo: 'リクエスト情報',
+      requestParams: 'リクエストパラメータ',
+      requestResult: 'リクエスト結果',
+      requestDuration: 'リクエスト期間',
+      normal: '正常',
+      exceptionTime: '例外時間',
+      processingStatus: '処理状態',
+      selectProcessingStatus: '処理状態を選択してください',
+      exceptionOccurrenceTime: '例外発生時間',
+      exceptionName: '例外名',
+      handler: '処理者',
+      redisVersion: 'Redisバージョン',
+      runningMode: '実行モード',
+      standalone: 'スタンドアロン',
+      cluster: 'クラスター',
+      port: 'ポート',
+      clientCount: 'クライアント数',
+      runningDays: '稼働日数',
+      usedMemory: '使用メモリ',
+      usedCPU: '使用CPU',
+      memoryConfig: 'メモリ設定',
+      isAOFEnabled: 'AOFが有効か',
+      isRDBSuccessful: 'RDBが成功か',
+      keyCount: 'キー数',
+      networkInOut: 'ネットワーク入出力',
+      memoryUsage: 'メモリ使用状況',
+      peak: 'ピーク',
+      commandStats: 'コマンド統計',
+      command: 'コマンド',
+      memoryConsumption: 'メモリ消費',
+      processed:'処理済み',
+      ignored:'無視され',
+      exceptionStack: '例外スタック',
+      test: 'テスト',
+      dragFilesHere: 'ファイルをここにドラッグするか、',
+      clickToUpload: 'クリックしてアップロード',
+      tipOnlyJPGPNGGIF: 'ヒント:jpg、png、gif形式のファイルのみが許可されています!',
+      uploadFailed: 'アップロードに失敗しました。もう一度アップロードしてください!',
+      onlyOneFileAllowed: 'アップロードできるファイルは1つだけです!'
+    },
+    system:{
+      menuName: 'メニュー名',
+      enterMenuName: 'メニュー名を入力してください',
+      status: 'ステータス',
+      expandCollapse: '展開/折りたたみ',
+      refreshMenuCache: 'メニューキャッシュをリフレッシュ',
+      icon: 'アイコン',
+      permissionIdentifier: '権限識別子',
+      enterPermissionIdentifier: '権限識別子を入力してください',
+      componentPath: 'コンポーネントパス',
+      componentName: 'コンポーネント名',
+      parentMenu: '親メニュー',
+      menuType: 'メニュータイプ',
+      menuIcon: 'メニューアイコン',
+      routeAddress: 'ルートアドレス',
+      enterRouteAddress: 'ルートアドレスを入力してください',
+      componentAddress: 'コンポーネントアドレス',
+      routingAccessAddress: 'ルーティングアクセスアドレス、例:user。外部ネットワークアドレスが必要な場合は、http(s):// で始まる必要があります',
+      controllerAuthCharacter: 'Controllerのメソッド上の権限文字列、例:@PreAuthorize(@ss.hasPermission(\'system:user:list\'))',
+      displayOrder: '表示順序',
+      menuStatus: 'メニューステータス',
+      displayStatus: '表示ステータス',
+      selectHidden: '非表示を選択すると、ルートはサイドバーに表示されませんが、アクセスは可能です',
+      show: '表示',
+      hide: '非表示',
+      alwaysShow: '常に表示',
+      selectNotWhen: '選択しない場合、そのメニューにサブメニューが1つだけの場合、自身を表示せずに直接サブメニューを表示します',
+      cacheStatus: 'キャッシュステータス',
+      selectCache: 'キャッシュを選択すると、`keep-alive`にキャッシュされます。「コンポーネント名」フィールドを必ず記入してください',
+      cache: 'キャッシュ',
+      noCache: 'キャッシュなし',
+      dictionaryName: '辞書名',
+      enterDictionaryName: '辞書名を入力してください',
+      dictionaryType: '辞書タイプ',
+      enterDictionaryType: '辞書タイプを入力してください',
+      selectDictionaryStatus: '辞書ステータスを選択してください',
+      dictionaryCode: '辞書コード',
+      dictionaryKey: '辞書キー',
+      dictionarySort: '辞書ソート',
+      colorType: '色タイプ',
+      enterParameterName: 'パラメータ名を入力してください',
+      enterContent: '内容を入力してください',
+      dictionaryLabel: '辞書ラベル',
+      enterDictionaryLabel: '辞書ラベルを入力してください',
+      dataStatus: 'データステータス',
+      dictionaryId: '辞書ID',
+      remark: '備考',
+      enterRemark: '備考を入力してください',
+      basePath: '基本パス',
+      enterBasePath: '基本パスを入力してください',
+      hostAddress: 'ホストアドレス',
+      enterHostAddress: 'ホストアドレスを入力してください',
+      hostPort: 'ホストポート',
+      enterHostPort: 'ホストポートを入力してください',
+      username: 'ユーザー名',
+      enterUsername: 'ユーザー名を入力してください',
+      password: 'パスワード',
+      enterPassword: 'パスワードを入力してください',
+      activeMode: 'アクティブモード',
+      passiveMode: 'パッシブモード',
+      announcementTitle: 'お知らせタイトル',
+      enterAnnouncementTitle: 'お知らせタイトルを入力してください',
+      selectAnnouncementStatus: 'お知らせステータスを選択してください',
+      announcementId: 'お知らせID',
+      announcementType: 'お知らせタイプ',
+      push: 'プッシュ',
+      pushSelectedNotification: '選択された通知をプッシュしますか?',
+      pushSuccess: 'プッシュ成功',
+      announcementContent: 'お知らせ内容',
+      selectAnnouncementType: 'お知らせタイプを選択してください',
+      selectStatus: 'ステータスを選択してください',
+      smsConfig: 'SMS設定',
+      smsType: 'SMSタイプ',
+      selectSmsType: 'SMSタイプを選択してください',
+      enableStatus: '有効ステータス',
+      selectEnableStatus: '有効ステータスを選択してください',
+      templateCode: 'テンプレートコード',
+      enterTemplateCode: 'テンプレートコードを入力してください',
+      smsApiTemplateNumber: 'SMS APIのテンプレート',
+      smsChannel: 'SMSチャンネル',
+      selectSmsChannel: 'SMSチャンネルを選択してください',
+      smsChannelId: 'SMSチャンネルID',
+      selectSmsChannelId: 'SMSチャンネルIDを選択してください',
+      selectSmsTypeAgain: '再度SMSタイプを選択してください',
+      templateId: 'テンプレートID',
+      enterTemplateId: 'テンプレートIDを入力してください',
+      templateName: 'テンプレート名',
+      enterTemplateName: 'テンプレート名を入力してください',
+      templateContent: 'テンプレート内容',
+      enterTemplateContent: 'テンプレート内容を入力してください',
+      test: 'テスト',
+      mobileNumber: '携帯番号',
+      enterMobileNumber: '携帯番号を入力してください',
+      parameter: 'パラメータ',
+      enter: '入力してください',
+      smsSignature: 'SMS名',
+      enterSmsSignature: 'SMS名を入力してください',
+      selectEnableState: '有効状態を選択してください',
+      serialNumber: 'No',
+      channelCode: 'チャネル',
+      enableState: '有効状態',
+      smsApiKey: 'SMS APIのアカウント',
+      smsApiSecret: 'SMS APIのシークレット',
+      smsCallbackUrl: 'SMS送信コールバックURL',
+      emailAccount: 'メールアカウント',
+      senderName: '送信者名',
+      recipientEmail: '受信者メール',
+      enterRecipientEmail: '受信者メールを入力してください',
+      emailParameter: 'メールパラメータ',
+      emailAddress: 'メールアドレス',
+      emailPassword: 'パスワード',
+      smtpServerDomain: 'SMTPサーバードメイン',
+      smtpServerPort: 'SMTPサーバーポート',
+      enableSSL: 'SSLを有効にする',
+      sendingTime: '送信時間',
+      receivingEmail: '受信メール',
+      userNumber: 'ユーザー番号',
+      userType: 'ユーザータイプ',
+      emailTitle: 'メールタイトル',
+      emailContent: 'メール内容',
+      emailParameterAgain: 'メールパラメータ',
+      sendingStatus: '送信ステータス',
+      emailAddressAgain: 'メールアドレス',
+      templateCodeAgain: 'テンプレートコード',
+      templateEncoding: 'エンコーディング',
+      templateSenderName: 'テンプレート送信者名',
+      messageNumberReturnedBySending: '送信によって返されたメッセージ番号',
+      sendingException: '送信例外',
+      data: 'データ',
+      receivingStatus: '受信ステータス',
+      receivingTime: '受信時間',
+      smsContent: 'SMS内容',
+      apiSendResult: 'API 送信結果',
+      apiSmsNumber: 'API SMS番号',
+      apiRequestNumber: 'API リクエスト番号',
+      apiReceiveStatus: 'API 受信状態',
+      apiReceiveResult: 'API 受信結果'
+    }
+  },
+  home: {
+    "today": "今日",
+    "salesVolume": "売上高",
+    "userVisits": "ユーザーアクセス数",
+    "orderVolume": "注文数",
+    "newUsers": "新規ユーザー",
+    "userManager": "旅行者",
+    "itineraryManager": "ツアー",
+    "guideManager": "ガイド",
+    "orderManager": "注文管理",
+    "paymentManager": "支払い管理",
+    "refundProcessing": "返金処理",
+    "operationalData": "運営データ",
+    "completedPayments": "支払完了の注文数",
+    "refundingOrders": "返金中の注文数",
+    "completedTrips": "旅行完了の注文数",
+    "attractions": "観光地数",
+    "itinerary": "ツアー数",
+    "registerGuide": "ガイド登録者数",
+    "memberOverview": "会員概要",
+    "registeredUsers": "登録旅行者数:",
+    "sequentialGrowthRate": "前期比:",
+    "visitors": "旅行者",
+    "activeUsers": "アクティブユーザー数:",
+    "placeOrder": "注文数",
+    "rechargedUsers": "チャージユーザー数:",
+    "averageOrderValue": "客単価:",
+    "completedCustomers": "成約ユーザー",
+    "transactionTrend": "取引トレンド",
+    "30days": "30日間",
+    "orderAmount": "注文金額",
+    "orderQuantity": "注文数",
+    "week": "週",
+    "lastWeekAmount": "先週の金額",
+    "thisWeekAmount": "今週の金額",
+    "lastWeekQuantity": "先週の数量",
+    "thisWeekQuantity": "今週の数量",
+    "month": "月",
+    "lastMonthAmount": "先月の金額",
+    "thisMonthAmount": "今月の金額",
+    "lastMonthQuantity": "先月の数量",
+    "thisMonthQuantity": "今月の数量",
+    "year": "年",
+    "lastYearAmount": "昨年の金額",
+    "thisYearAmount": "今年の金額",
+    "lastYearQuantity": "昨年の数量",
+    "thisYearQuantity": "今年の数量",
+    "yesterdayData": "昨日データ",
+    "quickAccess": "クイックアクセス",
+    "yesterday": "昨日",
+    "last7Days": "最近7日間",
+    "last30Days": "最近30日間",
+    "startDate": "開始日",
+    "endDate": "終了日",
+  },
+  aftersale:{
+    "guideName": "ガイド名",
+    "enterGuideName": "ガイド名を入力してください",
+    "refundNumber": "返金番号",
+    "enterRefundNumber": "返金番号を入力してください",
+    "orderNumber": "注文番号",
+    "enterOrderNumber": "注文番号を入力してください",
+    "afterSalesStatus": "状態",
+    "selectAfterSalesStatus": "状態を選択してください",
+    "all": "全て",
+    "afterSalesMethod": "方法",
+    "selectAfterSalesMethod": "方法を選択してください",
+    "creationTime": "作成時間",
+    "startDate": "開始日",
+    "endDate": "終了日",
+    "appointmentInfo": "予約情報",
+    "orderAmount": "注文金額",
+    "yen": "円",
+    "buyer": "購入者",
+    "applicationTime": "申請時間",
+    "processRefund": "返金処理",
+    "orderInfo": "注文情報",
+    "orderNo": "注文番号: ",
+    "orderType": "注文タイプ: ",
+    "buyerMessage": "購入者メッセージ: ",
+    "orderSource": "注文のソース: ",
+    "contactPhone": "連絡先電話番号: ",
+    "merchantRemark": "販売者の備考: ",
+    "paymentNumber": "支払い番号: ",
+    "paymentMethod": "支払い方法: ",
+    "afterSalesInfo": "アフターサービス情報",
+    "refundNumberLabel": "返金番号: ",
+    "afterSalesType": "アフターサービスのタイプ: ",
+    "refundAmount": "返金額: ",
+    "refundReason": "返金理由: ",
+    "additionalDescription": "追加説明: ",
+    "voucherImage": "エビデンス写真: ",
+    "refundStatus": "返金状態",
+    "agreeRefund": "返金に同意する",
+    "denyRefund": "返金を拒否する",
+    "confirmRefund": "返金を確認する",
+    "reminder": "リマインダー: ",
+    "guideDateReminder": "ガイド日がまだの場合、購入者に返金に同意してくださいをクリックします。",
+    "expiredReminder": "期限が切れている場合は、購入者と積極的に連絡を取ってください。",
+    "reserveGuideInfo": "予約ガイド情報",
+    "reservationInfo": "予約情報",
+    "reservationPrice": "予約価格",
+    "afterSalesLog": "アフターサービスログ",
+    "system": "システム",
+    "denyAfterSales": "アフターサービスを拒否する",
+    "approvalRemark": "承認備考",
+    "enterApprovalRemark": "承認備考を入力してください",
+    "afterSalesOrderNotFound": "アフターサービスの注文が見つかりません",
+    "agreeRefundPrompt": "返金に同意しますか?",
+    "confirmRefundPrompt": "返金を確認しますか?"
+  },
+  statistics:{
+    "guide_rank": "ガイドランキング",
+    "guide_id": "ID",
+    "guide_image": "画像",
+    "guide_nickname": "ニックネーム",
+    "views": "閲覧数",
+    "visitors": "訪問者数",
+    "added_to_cart": "カートに追加された予約数",
+    "ordered_items": "注文数",
+    "paid_items": "支払い済み予約数",
+    "payment_amount": "支払金額",
+    "refund_items": "返金件数",
+    "refund_amounts": "返金金額",
+    "favorites": "お気に入り数",
+    "conversion_rate": "訪問者-支払い転換率(%)",
+    "guide_overview": "ガイド概要",
+    "guide_page_views": "選択した条件下で、すべてのガイド/旅程詳細ページが訪問された回数。統計期間内に同一人物が複数回訪問した場合は複数回と数える。",
+    "guide_visitors": "選択した条件下で、任意のガイド/旅程詳細ページを訪問した人数。統計期間内に同一人物が複数回訪問した場合は一度だけと数える。",
+    "paid_items_count": "選択した条件下で、成功した支払い注文の件数の合計。",
+    "payment_total": "選択した条件下で、成功した支払い注文の金額の合計。",
+    "refund_count": "選択した条件下で、成功した返金の件数の合計。",
+    "refund_amount": "選択した条件下で、成功した返金の金額の合計。",
+    "quantity": "数量",
+    "amount": "金額",
+    "guide_status_excel": "ガイド状況.xls",
+    "attraction_rank": "観光地ランキング",
+    "attraction_id": "ID",
+    "attraction_image": "画像",
+    "attraction_name": "名前",
+    "attraction_overview": "観光地概要",
+    "attraction_page_views": "選択した条件下で、すべての観光地詳細ページが訪問された回数。統計期間内に同一人物が複数回訪問した場合は複数回と数える。",
+    "attraction_visitors": "選択した条件下で、任意の観光地詳細ページを訪問した人数。統計期間内に同一人物が複数回訪問した場合は一度だけと数える。",
+    "itinerary_rank": "行程ランキング",
+    "itinerary_id": "ID",
+    "itinerary_overview": "行程概要",
+    "itinerary_page_views": "選択した条件下で、すべての行程詳細ページが訪問された回数。統計期間内に同一人物が複数回訪問した場合は複数回と数える。",
+    "itinerary_visitors": "選択した条件下で、任意の行程詳細ページを訪問した人数。統計期間内に同一人物が複数回訪問した場合は一度だけと数える。",
+    "itinerary_status_excel": "行程状況.xls",
+    "attraction_status_excel": "観光地状況.xls",
+    "total_members": "累計会員数",
+    "total_spent": "累計消費金額",
+    "member_terminal": "会員端末",
+    "yesterday_order_count": "昨日の注文数",
+    "this_month_order_count": "今月の注文数",
+    "yesterday_payment": "昨日の支払金額",
+    "this_month_payment": "今月の支払金額",
+    "transaction_status": "取引状況",
+    "revenue": "売上高",
+    "guide_booking_payment": "ガイド予約支払金額",
+    "actual_payment": "ユーザーの購入商品の実際の支払金額(オフライン支払い注文はバックエンドで確認後に計上)",
+    "total_refund": "返金額",
+    "sales_revenue": "売上高",
+    "payment_received": "受け取った支払金額",
+    "recharge_amount": "チャージ金額",
+    "expense_amount": "支出金額"
+  },
+}

+ 688 - 3
src/locales/zh-CN.ts

@@ -1,5 +1,7 @@
 export default {
   common: {
+    systemName: '旅行者与导游匹配系统',
+    travelerDetail: '旅行者详情',
     inputText: '请输入',
     selectText: '请选择',
     startTimeText: '开始时间',
@@ -411,10 +413,33 @@ export default {
       createTime: '创建日期'
     },
     info: {
-      title: '基本信息',
+      title: '导游关联信息',
       basicInfo: '基本资料',
       resetPwd: '修改密码',
-      userSocial: '社交信息'
+      userSocial: '社交信息',
+      commentInfo: '评价资料',
+      Schedule: '基础日程表设定',
+      languageUsed: '使用语言',
+      textAndImageIntro: '图文自我介绍',
+      expertiseField: '擅长领域',
+      serviceItems: '服务项目',
+      precautions: '注意事项',
+      servicePrice: '服务价格',
+      maxParticipants: '对象上限人数',
+      region: '地域',
+      serviceStartDate: '导游服务开始日',
+      serviceEndDate: '导游服务终了日',
+      settlementBankInfo: '结算银行信息',
+      ratingStars: '评分星级',
+      expertiseStars: '专业知识星级',
+      serviceStars: '服务星级',
+      reviewContent: '评论内容',
+      likesCount: '点赞数',
+      work: '工作',
+      rest: '休息',
+      booked: '被预约了',
+      modifyDialog1: '不能修改今天之前的日期',
+      modifyDialog2: '该日期已被预约,无法修改'
     },
     rules: {
       nickname: '请输入用户昵称',
@@ -448,5 +473,665 @@ export default {
     btn_zoom_out: '缩小',
     preview: '预览'
   },
-  'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
+  'OAuth 2.0': 'OAuth 2.0', // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
+  guide: {
+    common: {
+      search: '搜索',
+      reset: '重置',
+      add: '新增',
+      export: '导出',
+      expandCollapse: '展开/折叠',
+      actions: '操作',
+      edit: '编辑',
+      delete: '删除',
+      confirm: '确 定',
+      cancel: '取 消'
+    },
+    category: {
+      chineseName: '中文名称',
+      enterChineseName: '请输入分类中文名称',
+      japaneseName: '日文名称',
+      enterJapaneseName: '请输入分类日文名称',
+      englishName: '英文名称',
+      enterEnglishName: '请输入分类英文名称',
+      creationTime: '创建时间',
+      startDate: '开始日期',
+      endDate: '结束日期',
+      parentCategoryID: '父分类编号',
+      categoryID: '分类编号',
+      mobileCategoryImage: '移动端分类图',
+      categorySort: '分类排序',
+      enableStatus: '开启状态',
+      selectParentCategoryID: '请选择父分类编号',
+      enterCategorySort: '请输入分类排序',
+      topTouristAttractionCategory: '顶级观光景点分类'
+    },
+    sight:{
+      id: '编号',
+      baseInfo: '基本情报',
+      attractionCategory: '景点分类',
+      region: '地域',
+      attractionInfo: '景点信息',
+      phoneNumber: '电话号码',
+      openingHours: '开放时间',
+      enterOpeningHours: '请输入开放时间',
+      multilingualExpansionInfo: '多语言扩充信息',
+      comments: '评论',
+      attractionCoverImage: '景点封面图',
+      enterPhoneNumber: '请输入电话号码',
+      attractionCarouselImages: '轮播图,最多6张',
+      officialWebsiteURL: '官网URL',
+      enterOfficialWebsiteURL: '请输入官网URL',
+      playTime: '游玩时间',
+      enterApproximatePlayTime: '请输入大致游玩所需时间',
+      sort: '排序',
+      selectATouristAttraction: '请选择一个观光景点',
+      selectAttractionCategory: '请选择景点分类',
+      ratingStars: '评分星级',
+      sceneryStars: '景色星级',
+      serviceStars: '服务星级',
+      reviewContent: '评论内容',
+      tourDuration: '游览时长',
+      likesCount: '点赞数',
+      enterLikesCount: '请输入点赞数',
+      image: '图片',
+      multilingualType: '多语言类型',
+      title: '标题',
+      introduction: '简介',
+      ticketsAndReservations: '门票&预约',
+      keywordGroup: '关键字组',
+      enterKeywords: '请输入关键字',
+      subtitle: '副标题',
+      attractionTextualDescription: '景点图文说明',
+      address: '地址',
+      additionalInfo: '补充说明'
+    },
+    trip:{
+      guideId: '导游编号',
+      enterGuideId: '请输入导游编号',
+      creationTime: '创建时间',
+      startDate: '开始日期',
+      endDate: '结束日期',
+      id: '编号',
+      guide: '导游',
+      coverImage: '封面图',
+      maxParticipants: '上限人数',
+      guidePrice: '导游价格',
+      multilingualExpansionInfo: '多语言扩充信息',
+      includedAttractions: '包含景点',
+      startTime: '开始时刻',
+      selectJourneyStartTime: '选择旅程开始时刻',
+      endTime: '结束时刻',
+      selectJourneyEndTime: '选择旅程结束时刻',
+      guideServicePrice: '导游服务价格',
+      enterGuideServicePrice: '请输入导游服务价格',
+      enableStatus: '开启状态',
+      importAttractions: '导入景点',
+      attractionCategory: '景点分类',
+      region: '地域',
+      attractionInfo: '景点信息',
+      openingHours: '开放时间',
+      sightseeingSpotId: '观光景点编号',
+      journeyStartTime: '旅程开始时间',
+      requiredTime: '所需时间',
+      selectJourneyBasicInfo: '请选择一个旅程基本情报',
+      multilingualType: '多语言类型',
+      multilingualTitle: '多语言标题',
+      includedItems: '包含项目',
+      excludedItems: '不包含项目',
+      featureTextualDescription: '特色图文描述',
+      additionalInfo: '补充说明'
+    },
+    user:{
+      userAccount: '用户账号',
+      enterUserAccount: '请输入用户账号',
+      mobileNumber: '手机号码',
+      enterMobileNumber: '请输入手机号码',
+      guideLanguage: '导游语言',
+      selectGuideLanguage: '请选择导游语言',
+      creationTime: '创建时间',
+      startDate: '开始日期',
+      endDate: '结束日期',
+      userId: '用户ID',
+      userNickname: '用户昵称',
+      lastLoginTime: '最后登录时间',
+      expertiseField: '擅长领域',
+      guideReviews: '导游评论',
+      userPassword: '用户密码',
+      enterUserPassword: '请输入用户密码',
+      enterUserNickname: '请输入用户昵称',
+      userEmail: '用户邮箱',
+      enterUserEmail: '请输入用户邮箱',
+      userGender: '用户性别',
+      pleaseSelect: '请选择',
+      selectGuideUsageLanguage: '请选择导游使用语言',
+      textAndImageIntro: '图文自我介绍',
+      serviceItems: '服务项目',
+      enterServiceItems: '请输入服务项目',
+      precautions: '注意事项',
+      enterServicePrecautions: '请输入提供服务时的注意事项',
+      guideServicePrice: '导游服务价格',
+      maxNumberOfPeople: '人数上限',
+      serviceTargetMaxNumber: '服务对象人数上限',
+      region: '地域',
+      guideServiceStartDate: '导游服务开始日',
+      selectGuideServiceStartDate: '选择导游服务开始日',
+      guideServiceEndDate: '导游服务终了日',
+      selectGuideServiceEndDate: '选择导游服务终了日',
+      bankInformation: '银行信息',
+      enterBankInformation: '请输入银行信息',
+      enterCorrectEmail: '请输入正确的邮箱地址',
+      enterCorrectMobileNumber: '请输入正确的手机号码',
+      id: '编号',
+      reviewer: '评价人',
+      type: '类型',
+      ratingStars: '评分星级',
+      expertiseStars: '专业知识星级',
+      serviceStars: '服务星级',
+      reviewTime: '评价时间',
+      reviewerId: '评价人ID',
+      enterReviewerUserId: '请输入评价人的用户编号',
+      enterReviewerName: '请输入评价人名称',
+      selectCustomerReviewSelfReview: '请选择客户评价/自我评价',
+      reviewContent: '评论内容',
+      likesCount: '点赞数',
+      registrationTime: '注册时间',
+      loginTime: '登录时间',
+      avatar: '头像',
+      status: '状态',
+      realName: '真实名字',
+      enterRealName: '请输入真实名字',
+      birthDate: '出生日期',
+      selectBirthDate: '选择出生日期',
+      basicInformation: '基本信息',
+      accountDetails: '账户明细',
+      orderManagement: '订单管理',
+      afterSalesManagement: '售后管理',
+      favoritesRecord: '收藏记录',
+      username: '用户名',
+      location: '所在地',
+      registrationIP: '注册 IP',
+    },
+    order:{
+      merchantOrderNumber: '商户单号',
+      enterMerchantOrderNumber: '请输入商户单号',
+      paymentOrderNumber: '支付单号',
+      enterPaymentOrderNumber: '请输入支付单号',
+      paymentStatus: '支付状态',
+      selectPaymentStatus: '请选择支付状态',
+      creationTime: '创建时间',
+      startDate: '开始日期',
+      endDate: '结束日期',
+      id: '编号',
+      paymentAmount: '支付金额',
+      refundAmount: '退款金额',
+      handlingFee: '手续金额',
+      orderNumber: '订单号',
+      merchant: '注文号',
+      payment: '支付号',
+      channel: '渠道',
+      paymentTime: '支付时间',
+      productTitle: '商品标题',
+      orderDetails: '订单详情',
+      handlingFeeRate: '手续费比例',
+      expirationTime: '失效时间',
+      updateTime: '更新时间',
+      successTime: '成功时间',
+      productDescription: '商品描述',
+      paymentIP: '支付 IP',
+      notificationURL: '通知 URL',
+      asyncCallbackContent: '支付通道异步回调内容',
+      refundOrderNumber: '退款单号',
+      enterRefundOrderNumber: '请输入退款单号',
+      refundStatus: '退款状态',
+      selectRefundStatus: '请选择退款状态',
+      details: '详情',
+      refundTime: '退款时间',
+      refundReason: '退款原因',
+      refundIP: '退款 IP',
+      channelErrorCode: '渠道错误码',
+      orderStatus: '订单状态',
+      all: '全部',
+      customTime: '自定义时间',
+      aggregateSearch: '聚合搜索',
+      enter: '请输入',
+      remark: '备注',
+      userUID: '用户UID',
+      userNickname: '用户昵称',
+      userPhone: '用户电话',
+      merchantRemark: '商家备注',
+      enterOrderRemark: '请输入订单备注',
+      reservationInfo: '预约信息',
+      reservationUnitPrice: '预约单价/导游日期',
+      afterSaleStatus: '售后状态',
+      orderPrice: '订单价格',
+      tourist: '游客',
+      orderTime: '下单时间',
+      paymentMethod: '支付方式',
+      yen: '円',
+      actualPayment: '实际支付',
+      orderInfo: '订单信息',
+      buyer: '买家',
+      orderType: '订单类型',
+      orderSource: '订单来源',
+      buyerMessage: '买家留言',
+      reservationGuideInfo: '预约导游信息',
+      totalAmountOfGoods: '商品总额',
+      totalPayment: '支付总额',
+      visitorInfo: '游客信息',
+      orderOperationLog: '订单操作日志',
+      notpay: '未支付',
+      reservationdate: '预约日期'
+    },
+    infra:{
+      configName: '配置名',
+      enterConfigName: '请输入配置名',
+      storage: '存储器',
+      selectStorage: '请选择存储器',
+      creationTime: '创建时间',
+      startDate: '开始日期',
+      endDate: '结束日期',
+      id: '编号',
+      remark: '备注',
+      mainConfig: '主配置',
+      enterRemark: '请输入备注',
+      basePath: '基础路径',
+      enterBasePath: '请输入基础路径',
+      hostAddress: '主机地址',
+      enterHostAddress: '请输入主机地址',
+      hostPort: '主机端口',
+      enterHostPort: '请输入主机端口',
+      username: '用户名',
+      enterUsername: '请输入用户名',
+      enterPassword: '请输入密码',
+      password: '密码',
+      activeMode: '主动模式',
+      passiveMode: '被动模式',
+      nodeAddress: '节点地址',
+      enterNodeAddress: '请输入节点地址',
+      storageBucket: '存储 bucket',
+      enterBucket: '请输入 bucket',
+      enterAccessKey: '请输入 accessKey',
+      enterAccessSecret: '请输入 accessSecret',
+      customDomain: '自定义域名',
+      filePath: '文件路径',
+      enterFilePath: '请输入文件路径',
+      fileType: '文件类型',
+      enterFileType: '请输入文件类型',
+      uploadFile: '上传文件',
+      fileSize: '文件大小',
+      fileName: '文件名',
+      fileContent: '文件内容',
+      preview: '预览',
+      download: '下载',
+      uploadTime: '上传时间',
+      userId: '用户编号',
+      enterUserId: '请输入用户编号',
+      executionDuration: '执行时长',
+      requestTime: '请求时间',
+      enterExecutionDuration: '请输入执行时长',
+      resultCode: '结果码',
+      enterResultCode: '请输入结果码',
+      logId: '日志编号',
+      requestMethod: '请求方法',
+      requestAddress: '请求地址',
+      operationResult: '操作结果',
+      success: '成功',
+      failure: '失败',
+      operationModule: '操作模块',
+      operationName: '操作名',
+      operationType: '操作类型',
+      operation: '操作',
+      logPrimaryKey: '日志主键',
+      linkTracing: '链路追踪',
+      userIP: '用户 IP',
+      userInfo: '用户信息',
+      userUA: '用户 UA',
+      requestInfo: '请求信息',
+      requestParams: '请求参数',
+      requestResult: '请求结果',
+      requestDuration: '请求耗时',
+      normal: '正常',
+      exceptionTime: '异常时间',
+      processingStatus: '处理状态',
+      selectProcessingStatus: '请选择处理状态',
+      exceptionOccurrenceTime: '异常发生时间',
+      exceptionName: '异常名',
+      handler: '处理人',
+      redisVersion: 'Redis版本',
+      runningMode: '运行模式',
+      standalone: '单机',
+      cluster: '集群',
+      port: '端口',
+      clientCount: '客户端数',
+      runningDays: '运行时间(天)',
+      usedMemory: '使用内存',
+      usedCPU: '使用CPU',
+      memoryConfig: '内存配置',
+      isAOFEnabled: 'AOF是否开启',
+      isRDBSuccessful: 'RDB是否成功',
+      keyCount: 'Key数量',
+      networkInOut: '网络入口/出口',
+      memoryUsage: '内存使用情况',
+      peak: '峰值',
+      commandStats: '命令统计',
+      command: '命令',
+      memoryConsumption: '内存消耗',
+      processed:'已处理',
+      ignored:'已忽略',
+      exceptionStack: '异常堆栈',
+      test:'测试',
+      dragFilesHere: '将文件拖到此处,或',
+      clickToUpload: '点击上传',
+      tipOnlyJPGPNGGIF: '提示:仅允许导入 jpg、png、gif 格式文件!',
+      uploadFailed: '上传失败,请您重新上传!',
+      onlyOneFileAllowed: '最多只能上传一个文件!'
+    },
+    system:{
+      menuName: '菜单名称',
+      enterMenuName: '请输入菜单名称',
+      status: '状态',
+      expandCollapse: '展开/折叠',
+      refreshMenuCache: '刷新菜单缓存',
+      icon: '图标',
+      permissionIdentifier: '权限标识',
+      enterPermissionIdentifier: '请输入权限标识',
+      componentPath: '组件路径',
+      componentName: '组件名称',
+      parentMenu: '上级菜单',
+      menuType: '菜单类型',
+      menuIcon: '菜单图标',
+      routeAddress: '路由地址',
+      enterRouteAddress: '请输入路由地址',
+      componentAddress: '组件地址',
+      routingAccessAddress: '访问的路由地址,如:user。如需外网地址时,则以 http(s):// 开头',
+      controllerAuthCharacter: 'Controller 方法上的权限字符,如:@PreAuthorize(@ss.hasPermission(\'system:user:list\'))',
+      displayOrder: '显示排序',
+      menuStatus: '菜单状态',
+      displayStatus: '显示状态',
+      selectHidden: '选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问',
+      show: '显示',
+      hide: '隐藏',
+      alwaysShow: '总是显示',
+      selectNotWhen: '选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单',
+      cacheStatus: '缓存状态',
+      selectCache: '选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段',
+      cache: '缓存',
+      noCache: '不缓存',
+      dictionaryName: '字典名称',
+      enterDictionaryName: '请输入字典名称',
+      dictionaryType: '字典类型',
+      enterDictionaryType: '请输入字典类型',
+      selectDictionaryStatus: '请选择字典状态',
+      dictionaryCode: '字典编码',
+      dictionaryKey: '字典键值',
+      dictionarySort: '字典排序',
+      colorType: '颜色类型',
+      enterParameterName: '请输入参数名称',
+      enterContent: '请输入内容',
+      dictionaryLabel: '字典标签',
+      enterDictionaryLabel: '请输入字典标签',
+      dataStatus: '数据状态',
+      dictionaryId: '字典编号',
+      remark: '备注',
+      enterRemark: '请输入备注',
+      basePath: '基础路径',
+      enterBasePath: '请输入基础路径',
+      hostAddress: '主机地址',
+      enterHostAddress: '请输入主机地址',
+      hostPort: '主机端口',
+      enterHostPort: '请输入主机端口',
+      username: '用户名',
+      enterUsername: '请输入用户名',
+      password: '密码',
+      enterPassword: '请输入密码',
+      activeMode: '主动模式',
+      passiveMode: '被动模式',
+      announcementTitle: '公告标题',
+      enterAnnouncementTitle: '请输入公告标题',
+      selectAnnouncementStatus: '请选择公告状态',
+      announcementId: '公告编号',
+      announcementType: '公告类型',
+      push: '推送',
+      pushSelectedNotification: '是否推送所选中通知?',
+      pushSuccess: '推送成功',
+      announcementContent: '公告内容',
+      selectAnnouncementType: '请选择公告类型',
+      selectStatus: '请选择状态',
+      smsConfig: '短信配置',
+      smsType: '短信类型',
+      selectSmsType: '请选择短信类型',
+      enableStatus: '开启状态',
+      selectEnableStatus: '请选择开启状态',
+      templateCode: '模板编码',
+      enterTemplateCode: '请输入模板编码',
+      smsApiTemplateNumber: '短信 API 的模板编号',
+      smsChannel: '短信渠道',
+      selectSmsChannel: '请选择短信渠道',
+      smsChannelId: '短信渠道编号',
+      selectSmsChannelId: '请选择短信渠道编号',
+      selectSmsTypeAgain: '请选择短信类型',
+      templateId: '模板编号',
+      enterTemplateId: '请输入模板编号',
+      templateName: '模板名称',
+      enterTemplateName: '请输入模板名称',
+      templateContent: '模板内容',
+      enterTemplateContent: '请输入模板内容',
+      test: '测试',
+      mobileNumber: '手机号',
+      enterMobileNumber: '请输入手机号',
+      parameter: '参数',
+      enter: '请输入',
+      smsSignature: '短信签名',
+      enterSmsSignature: '请输入短信签名',
+      selectEnableState: '请选择启用状态',
+      serialNumber: '编号',
+      channelCode: '渠道编码',
+      enableState: '启用状态',
+      smsApiKey: '短信 API 的账号',
+      smsApiSecret: '短信 API 的密钥',
+      smsCallbackUrl: '短信发送回调 URL',
+      emailAccount: '邮箱账号',
+      senderName: '发送人名称',
+      recipientEmail: '收件邮箱',
+      enterRecipientEmail: '请输入收件邮箱',
+      emailParameter: '邮箱参数',
+      emailAddress: '邮箱',
+      emailPassword: '密码',
+      smtpServerDomain: 'SMTP 服务器域名',
+      smtpServerPort: 'SMTP 服务器端口',
+      enableSSL: '是否开启 SSL',
+      sendingTime: '发送时间',
+      receivingEmail: '接收邮箱',
+      userNumber: '用户编号',
+      userType: '用户类型',
+      emailTitle: '邮件标题',
+      emailContent: '邮件内容',
+      sendingStatus: '发送状态',
+      emailAddressAgain: '发送邮箱地址',
+      templateCodeAgain: '模板编号',
+      templateEncoding: '模板编码',
+      templateSenderName: '模版发送人名称',
+      messageNumberReturnedBySending: '发送返回的消息编号',
+      sendingException: '发送异常',
+      data: '数据',
+      receivingStatus: '接收状态',
+      receivingTime: '接收时间',
+      smsContent: '短信内容',
+      apiSendResult: 'API 发送结果',
+      apiSmsNumber: 'API 短信编号',
+      apiRequestNumber: 'API 请求编号',
+      apiReceiveStatus: 'API 接收状态',
+      apiReceiveResult: 'API 接收结果',
+    }
+  },
+  home: {
+    "today": "今日",
+    "salesVolume": "销售额",
+    "userVisits": "用户访问量",
+    "orderVolume": "订单量",
+    "newUsers": "新增用户",
+    "userManager": "用户管理",
+    "itineraryManager": "行程管理",
+    "guideManager": "导游管理",
+    "orderManager": "注文管理",
+    "paymentManager": "支付管理",
+    "refundProcessing": "返金处理",
+    "operationalData": "运营数据",
+    "completedPayments": "支付完了订单",
+    "refundingOrders": "退款中订单",
+    "completedTrips": "旅游完了订单",
+    "attractions": "景点",
+    "itinerary": "行程",
+    "registerGuide": "注册导游",
+    "memberOverview": "会员概览",
+    "registeredUsers": "注册用户数量",
+    "sequentialGrowthRate": "环比增长率:",
+    "visitors": "访客",
+    "activeUsers": "活跃用户数量:",
+    "placeOrder": "下单",
+    "rechargedUsers": "充值用户数量:",
+    "averageOrderValue": "客单价:",
+    "completedCustomers": "成交用户:",
+    "transactionTrend": "交易量趋势",
+    "30days": "30天",
+    "orderAmount": "订单金额",
+    "orderQuantity": "订单数量",
+    "week": "周",
+    "lastWeekAmount": "上周金额",
+    "thisWeekAmount": "本周金额",
+    "lastWeekQuantity": "上周数量",
+    "thisWeekQuantity": "本周数量",
+    "month": "月",
+    "lastMonthAmount": "上月金额",
+    "thisMonthAmount": "本月金额",
+    "lastMonthQuantity": "上月数量",
+    "thisMonthQuantity": "本月数量",
+    "year": "年",
+    "lastYearAmount": "去年金额",
+    "thisYearAmount": "今年金额",
+    "lastYearQuantity": "去年数量",
+    "thisYearQuantity": "今年数量",
+    "yesterdayData": "昨日数据",
+    "quickAccess": "快捷入口",
+    "yesterday": "昨天",
+    "last7Days": "最近7天",
+    "last30Days": "最近30天",
+    "startDate": "开始日期",
+    "endDate": "结束日期",
+  },
+  aftersale:{
+    "guideName": "导游名称",
+    "enterGuideName": "请输入导游名称",
+    "refundNumber": "退款编号",
+    "enterRefundNumber": "请输入退款编号",
+    "orderNumber": "订单编号",
+    "enterOrderNumber": "请输入订单编号",
+    "afterSalesStatus": "售后状态",
+    "selectAfterSalesStatus": "请选择售后状态",
+    "all": "全部",
+    "afterSalesMethod": "售后方式",
+    "selectAfterSalesMethod": "请选择售后方式",
+    "creationTime": "创建时间",
+    "startDate": "开始日期",
+    "endDate": "结束日期",
+    "appointmentInfo": "预约信息",
+    "orderAmount": "订单金额",
+    "yen": "日元",
+    "buyer": "买家",
+    "applicationTime": "申请时间",
+    "processRefund": "处理退款",
+    "orderInfo": "订单信息",
+    "orderNo": "订单号: ",
+    "orderType": "订单类型: ",
+    "buyerMessage": "买家留言: ",
+    "orderSource": "订单来源: ",
+    "contactPhone": "联系电话: ",
+    "merchantRemark": "商家备注: ",
+    "paymentNumber": "支付单号: ",
+    "paymentMethod": "付款方式: ",
+    "afterSalesInfo": "售后信息",
+    "refundNumberLabel": "退款编号: ",
+    "afterSalesType": "售后类型: ",
+    "refundAmount": "退款金额: ",
+    "refundReason": "退款原因: ",
+    "additionalDescription": "补充描述: ",
+    "voucherImage": "凭证图片: ",
+    "refundStatus": "退款状态",
+    "agreeRefund": "同意退款",
+    "denyRefund": "拒绝退款",
+    "confirmRefund": "确认退款",
+    "reminder": "提醒: ",
+    "guideDateReminder": "如果未到导游日期,请点击同意退款给买家。",
+    "expiredReminder": "如果已经过期,请主动与买家联系。",
+    "reserveGuideInfo": "预约导游信息",
+    "reservationInfo": "预约情报",
+    "reservationPrice": "预约价格",
+    "afterSalesLog": "售后日志",
+    "system": "系",
+    "denyAfterSales": "拒绝售后",
+    "approvalRemark": "审批备注",
+    "enterApprovalRemark": "请输入审批备注",
+    "afterSalesOrderNotFound": "售后订单不存在",
+    "agreeRefundPrompt": "是否同意退款?",
+    "confirmRefundPrompt": "是否确认退款?"
+  },
+  statistics:{
+    "guide_rank": "导游排行",
+    "guide_id": "导游ID",
+    "guide_image": "导游图",
+    "guide_nickname": "导游昵称",
+    "views": "浏览量",
+    "visitors": "访客数",
+    "added_to_cart": "加购件数",
+    "ordered_items": "下单件数",
+    "paid_items": "支付件数",
+    "payment_amount": "支付金额",
+    "refund_items": "退款件数",
+    "refund_amounts": "退款金额",
+    "favorites": "收藏数",
+    "conversion_rate": "访客-支付转化率(%)",
+    "guide_overview": "导游概况",
+    "guide_page_views": "在选定条件下,所有导游/行程详情页被访问的次数,一个人在统计时间内访问多次记为多次。",
+    "guide_visitors": "在选定条件下,访问任何导游/行程详情页的人数,一个人在统计时间范围内访问多次只记为一个。",
+    "paid_items_count": "在选定条件下,成功付款订单的件数之和。",
+    "payment_total": "在选定条件下,成功付款订单的金额之和。",
+    "refund_count": "在选定条件下,成功退款的件数之和。",
+    "refund_amount": "在选定条件下,成功退款的金额之和。",
+    "quantity": "数量",
+    "amount": "金额",
+    "guide_status_excel": "导游状况.xls",
+    "attraction_rank": "景点排行",
+    "attraction_id": "景点ID",
+    "attraction_image": "景点图",
+    "attraction_name": "名称",
+    "attraction_overview": "景点概况",
+    "attraction_page_views": "在选定条件下,所有景点详情页被访问的次数,一个人在统计时间内访问多次记为多次。",
+    "attraction_visitors": "在选定条件下,访问任何景点详情页的人数,一个人在统计时间范围内访问多次只记为一个。",
+    "itinerary_rank": "行程排行",
+    "itinerary_id": "行程ID",
+    "itinerary_overview": "行程概况",
+    "itinerary_page_views": "在选定条件下,所有行程详情页被访问的次数,一个人在统计时间内访问多次记为多次。",
+    "itinerary_visitors": "在选定条件下,访问任何行程详情页的人数,一个人在统计时间范围内访问多次只记为一个。",
+    "itinerary_status_excel": "行程状况.xls",
+    "attraction_status_excel": "景点状况.xls",
+    "total_members": "累计会员数",
+    "total_spent": "累计消费金额",
+    "member_terminal": "会员终端",
+    "yesterday_order_count": "昨日订单数量",
+    "this_month_order_count": "本月订单数量",
+    "yesterday_payment": "昨日支付金额",
+    "this_month_payment": "本月支付金额",
+    "transaction_status": "交易状况",
+    "revenue": "营业额",
+    "guide_booking_payment": "导游预约支付金额",
+    "actual_payment": "用户购买商品的实际支付金额,(线下支付订单在后台确认支付后计入)",
+    "total_refund": "退款金额",
+    "sales_revenue": "营业额",
+    "payment_received": "支付金额",
+    "recharge_amount": "充值金额",
+    "expense_amount": "支出金额"
+
+
+  },
 }

+ 24 - 3
src/router/modules/remaining.ts

@@ -59,7 +59,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     children: [
       {
         path: 'index',
-        component: () => import('@/views/Home/Index.vue'),
+        component: () => import('@/views/Home/index.vue'),
         name: 'Index',
         meta: {
           title: t('router.home'),
@@ -127,7 +127,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
-
   {
     path: '/codegen',
     component: Layout,
@@ -412,6 +411,28 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
+  {
+    path: '/guide/trade', // 交易中心
+    component: Layout,
+    name: 'GuideTradeCenter',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'order/detail/:id(\\d+)',
+        component: () => import('@/views/guide/trade/order/detail/index.vue'),
+        name: 'GuideTradeOrderDetail',
+        meta: { title: '订单详情', icon: 'ep:view', activeMenu: '/guide/trade/order' }
+      },
+      {
+        path: 'after-sale/detail/:id(\\d+)',
+        component: () => import('@/views/guide/trade/afterSale/detail/index.vue'),
+        name: 'GuideTradeAfterSaleDetail',
+        meta: { title: '退款详情', icon: 'ep:view', activeMenu: '/guide/trade/after-sale' }
+      }
+    ]
+  },
   {
     path: '/member',
     component: Layout,
@@ -422,7 +443,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         path: 'user/detail/:id',
         name: 'MemberUserDetail',
         meta: {
-          title: '会员详情',
+          title: t('common.travelerDetail'),
           noCache: true,
           hidden: true
         },

+ 5 - 1
src/store/modules/locale.ts

@@ -20,7 +20,7 @@ export const useLocaleStore = defineStore('locales', {
   state: (): LocaleState => {
     return {
       currentLocale: {
-        lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN',
+        lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN' ,
         elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN']
       },
       // 多语言
@@ -32,6 +32,10 @@ export const useLocaleStore = defineStore('locales', {
         {
           lang: 'en',
           name: 'English'
+        },
+        {
+          lang: 'ja',
+          name: '日本語'
         }
       ]
     }

+ 1 - 0
src/types/components.d.ts

@@ -24,6 +24,7 @@ export type ComponentName =
   | 'UploadImg'
   | 'UploadImgs'
   | 'UploadFile'
+  | 'QEditor'
 
 export type ColProps = {
   span?: number

+ 5 - 1
src/utils/dict.ts

@@ -209,5 +209,9 @@ export enum DICT_TYPE {
 
   // ========== ERP - 企业资源计划模块  ==========
   ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态
-  ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type' // 库存明细的业务类型
+  ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type', // 库存明细的业务类型
+
+  // ========== 导游配对模块  ==========
+  I8N_TYPE = 'i8n_type',
+  GUIDE_COMMENT_TYPE = 'guide_comment_type'
 }

+ 1 - 0
src/utils/tree.ts

@@ -14,6 +14,7 @@ export const defaultProps = {
   label: 'name',
   value: 'id',
   isLeaf: 'leaf',
+  multiple: true,
   emitPath: false // 用于 cascader 组件:在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值
 }
 

+ 384 - 0
src/views/Home BK/Index.vue

@@ -0,0 +1,384 @@
+<template>
+  <div>
+    <el-card shadow="never">
+      <el-skeleton :loading="loading" animated>
+        <el-row :gutter="16" justify="space-between">
+          <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
+            <div class="flex items-center">
+              <el-avatar :src="avatar" :size="70" class="mr-16px">
+                <img src="@/assets/imgs/avatar.gif" alt="" />
+              </el-avatar>
+              <div>
+                <div class="text-20px">
+                  {{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
+                </div>
+                <div class="mt-10px text-14px text-gray-500">
+                  {{ t('workplace.toady') }},20℃ - 32℃!
+                </div>
+              </div>
+            </div>
+          </el-col>
+          <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
+            <div class="h-70px flex items-center justify-end lt-sm:mt-10px">
+              <div class="px-8px text-right">
+                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
+                <CountTo
+                  class="text-20px"
+                  :start-val="0"
+                  :end-val="totalSate.project"
+                  :duration="2600"
+                />
+              </div>
+              <el-divider direction="vertical" />
+              <div class="px-8px text-right">
+                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
+                <CountTo
+                  class="text-20px"
+                  :start-val="0"
+                  :end-val="totalSate.todo"
+                  :duration="2600"
+                />
+              </div>
+              <el-divider direction="vertical" border-style="dashed" />
+              <div class="px-8px text-right">
+                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
+                <CountTo
+                  class="text-20px"
+                  :start-val="0"
+                  :end-val="totalSate.access"
+                  :duration="2600"
+                />
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </el-skeleton>
+    </el-card>
+  </div>
+
+  <el-row class="mt-8px" :gutter="8" justify="space-between">
+    <el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
+      <el-card shadow="never">
+        <template #header>
+          <div class="h-3 flex justify-between">
+            <span>{{ t('workplace.project') }}</span>
+            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
+          </div>
+        </template>
+        <el-skeleton :loading="loading" animated>
+          <el-row>
+            <el-col
+              v-for="(item, index) in projects"
+              :key="`card-${index}`"
+              :xl="8"
+              :lg="8"
+              :md="8"
+              :sm="24"
+              :xs="24"
+            >
+              <el-card shadow="hover">
+                <div class="flex items-center">
+                  <Icon :icon="item.icon" :size="25" class="mr-8px" />
+                  <span class="text-16px">{{ item.name }}</span>
+                </div>
+                <div class="mt-16px text-14px text-gray-400">{{ t(item.message) }}</div>
+                <div class="mt-16px flex justify-between text-12px text-gray-400">
+                  <span>{{ item.personal }}</span>
+                  <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
+                </div>
+              </el-card>
+            </el-col>
+          </el-row>
+        </el-skeleton>
+      </el-card>
+
+      <el-card shadow="never" class="mt-8px">
+        <el-skeleton :loading="loading" animated>
+          <el-row :gutter="20" justify="space-between">
+            <el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
+              <el-card shadow="hover" class="mb-8px">
+                <el-skeleton :loading="loading" animated>
+                  <Echart :options="pieOptionsData" :height="280" />
+                </el-skeleton>
+              </el-card>
+            </el-col>
+            <el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
+              <el-card shadow="hover" class="mb-8px">
+                <el-skeleton :loading="loading" animated>
+                  <Echart :options="barOptionsData" :height="280" />
+                </el-skeleton>
+              </el-card>
+            </el-col>
+          </el-row>
+        </el-skeleton>
+      </el-card>
+    </el-col>
+    <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
+      <el-card shadow="never">
+        <template #header>
+          <div class="h-3 flex justify-between">
+            <span>{{ t('workplace.shortcutOperation') }}</span>
+          </div>
+        </template>
+        <el-skeleton :loading="loading" animated>
+          <el-row>
+            <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
+              <div class="flex items-center">
+                <Icon :icon="item.icon" class="mr-8px" />
+                <el-link type="default" :underline="false" @click="setWatermark(item.name)">
+                  {{ item.name }}
+                </el-link>
+              </div>
+            </el-col>
+          </el-row>
+        </el-skeleton>
+      </el-card>
+      <el-card shadow="never" class="mt-8px">
+        <template #header>
+          <div class="h-3 flex justify-between">
+            <span>{{ t('workplace.notice') }}</span>
+            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
+          </div>
+        </template>
+        <el-skeleton :loading="loading" animated>
+          <div v-for="(item, index) in notice" :key="`dynamics-${index}`">
+            <div class="flex items-center">
+              <el-avatar :src="avatar" :size="35" class="mr-16px">
+                <img src="@/assets/imgs/avatar.gif" alt="" />
+              </el-avatar>
+              <div>
+                <div class="text-14px">
+                  <Highlight :keys="item.keys.map((v) => t(v))">
+                    {{ item.type }} : {{ item.title }}
+                  </Highlight>
+                </div>
+                <div class="mt-16px text-12px text-gray-400">
+                  {{ formatTime(item.date, 'yyyy-MM-dd') }}
+                </div>
+              </div>
+            </div>
+            <el-divider />
+          </div>
+        </el-skeleton>
+      </el-card>
+    </el-col>
+  </el-row>
+</template>
+<script lang="ts" setup>
+import { set } from 'lodash-es'
+import { EChartsOption } from 'echarts'
+import { formatTime } from '@/utils'
+
+import { useUserStore } from '@/store/modules/user'
+import { useWatermark } from '@/hooks/web/useWatermark'
+import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
+import { pieOptions, barOptions } from './echarts-data'
+
+defineOptions({ name: 'Home' })
+
+const { t } = useI18n()
+const userStore = useUserStore()
+const { setWatermark } = useWatermark()
+const loading = ref(true)
+const avatar = userStore.getUser.avatar
+const username = userStore.getUser.nickname
+const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
+// 获取统计数
+let totalSate = reactive<WorkplaceTotal>({
+  project: 0,
+  access: 0,
+  todo: 0
+})
+
+const getCount = async () => {
+  const data = {
+    project: 40,
+    access: 2340,
+    todo: 10
+  }
+  totalSate = Object.assign(totalSate, data)
+}
+
+// 获取项目数
+let projects = reactive<Project[]>([])
+const getProject = async () => {
+  const data = [
+    {
+      name: 'Github',
+      icon: 'akar-icons:github-fill',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'Vue',
+      icon: 'logos:vue',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'Angular',
+      icon: 'logos:angular-icon',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'React',
+      icon: 'logos:react',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'Webpack',
+      icon: 'logos:webpack',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    },
+    {
+      name: 'Vite',
+      icon: 'vscode-icons:file-type-vite',
+      message: 'workplace.introduction',
+      personal: 'Archer',
+      time: new Date()
+    }
+  ]
+  projects = Object.assign(projects, data)
+}
+
+// 获取通知公告
+let notice = reactive<Notice[]>([])
+const getNotice = async () => {
+  const data = [
+    {
+      title: '系统升级版本',
+      type: '通知',
+      keys: ['通知', '升级'],
+      date: new Date()
+    },
+    {
+      title: '系统凌晨维护',
+      type: '公告',
+      keys: ['公告', '维护'],
+      date: new Date()
+    },
+    {
+      title: '系统升级版本',
+      type: '通知',
+      keys: ['通知', '升级'],
+      date: new Date()
+    },
+    {
+      title: '系统凌晨维护',
+      type: '公告',
+      keys: ['公告', '维护'],
+      date: new Date()
+    }
+  ]
+  notice = Object.assign(notice, data)
+}
+
+// 获取快捷入口
+let shortcut = reactive<Shortcut[]>([])
+
+const getShortcut = async () => {
+  const data = [
+    {
+      name: 'Github',
+      icon: 'akar-icons:github-fill',
+      url: 'github.io'
+    },
+    {
+      name: 'Vue',
+      icon: 'logos:vue',
+      url: 'vuejs.org'
+    },
+    {
+      name: 'Vite',
+      icon: 'vscode-icons:file-type-vite',
+      url: 'https://vitejs.dev/'
+    },
+    {
+      name: 'Angular',
+      icon: 'logos:angular-icon',
+      url: 'github.io'
+    },
+    {
+      name: 'React',
+      icon: 'logos:react',
+      url: 'github.io'
+    },
+    {
+      name: 'Webpack',
+      icon: 'logos:webpack',
+      url: 'github.io'
+    }
+  ]
+  shortcut = Object.assign(shortcut, data)
+}
+
+// 用户来源
+const getUserAccessSource = async () => {
+  const data = [
+    { value: 335, name: 'analysis.directAccess' },
+    { value: 310, name: 'analysis.mailMarketing' },
+    { value: 234, name: 'analysis.allianceAdvertising' },
+    { value: 135, name: 'analysis.videoAdvertising' },
+    { value: 1548, name: 'analysis.searchEngines' }
+  ]
+  set(
+    pieOptionsData,
+    'legend.data',
+    data.map((v) => t(v.name))
+  )
+  pieOptionsData!.series![0].data = data.map((v) => {
+    return {
+      name: t(v.name),
+      value: v.value
+    }
+  })
+}
+const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
+
+// 周活跃量
+const getWeeklyUserActivity = async () => {
+  const data = [
+    { value: 13253, name: 'analysis.monday' },
+    { value: 34235, name: 'analysis.tuesday' },
+    { value: 26321, name: 'analysis.wednesday' },
+    { value: 12340, name: 'analysis.thursday' },
+    { value: 24643, name: 'analysis.friday' },
+    { value: 1322, name: 'analysis.saturday' },
+    { value: 1324, name: 'analysis.sunday' }
+  ]
+  set(
+    barOptionsData,
+    'xAxis.data',
+    data.map((v) => t(v.name))
+  )
+  set(barOptionsData, 'series', [
+    {
+      name: t('analysis.activeQuantity'),
+      data: data.map((v) => v.value),
+      type: 'bar'
+    }
+  ])
+}
+
+const getAllApi = async () => {
+  await Promise.all([
+    getCount(),
+    getProject(),
+    getNotice(),
+    getShortcut(),
+    getUserAccessSource(),
+    getWeeklyUserActivity()
+  ])
+  loading.value = false
+}
+
+getAllApi()
+</script>

+ 0 - 0
src/views/Home/Index2.vue → src/views/Home BK/Index2.vue


+ 0 - 0
src/views/Home/echarts-data.ts → src/views/Home BK/echarts-data.ts


+ 55 - 0
src/views/Home BK/types.ts

@@ -0,0 +1,55 @@
+export type WorkplaceTotal = {
+  project: number
+  access: number
+  todo: number
+}
+
+export type Project = {
+  name: string
+  icon: string
+  message: string
+  personal: string
+  time: Date | number | string
+}
+
+export type Notice = {
+  title: string
+  type: string
+  keys: string[]
+  date: Date | number | string
+}
+
+export type Shortcut = {
+  name: string
+  icon: string
+  url: string
+}
+
+export type RadarData = {
+  personal: number
+  team: number
+  max: number
+  name: string
+}
+export type AnalysisTotalTypes = {
+  users: number
+  messages: number
+  moneys: number
+  shoppings: number
+}
+
+export type UserAccessSource = {
+  value: number
+  name: string
+}
+
+export type WeeklyUserActivity = {
+  value: number
+  name: string
+}
+
+export type MonthlySales = {
+  name: string
+  estimate: number
+  actual: number
+}

+ 105 - 341
src/views/Home/Index.vue

@@ -1,4 +1,5 @@
 <template>
+  <!-- <doc-alert title="商城手册(功能开启)" url="https://doc.iocoder.cn/mall/build/" /> -->
   <div>
     <el-card shadow="never">
       <el-skeleton :loading="loading" animated>
@@ -10,375 +11,138 @@
               </el-avatar>
               <div>
                 <div class="text-20px">
-                  {{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
+                  {{ t('workplace.welcome') }} {{ username }} 
                 </div>
                 <div class="mt-10px text-14px text-gray-500">
-                  {{ t('workplace.toady') }},20℃ - 32℃!
+                  {{ t('workplace.happyDay') }}
+                  <!-- {{ t('workplace.toady') }},20℃ - 32℃! -->
                 </div>
               </div>
             </div>
           </el-col>
-          <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
-            <div class="h-70px flex items-center justify-end lt-sm:mt-10px">
-              <div class="px-8px text-right">
-                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
-                <CountTo
-                  class="text-20px"
-                  :start-val="0"
-                  :end-val="totalSate.project"
-                  :duration="2600"
-                />
-              </div>
-              <el-divider direction="vertical" />
-              <div class="px-8px text-right">
-                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
-                <CountTo
-                  class="text-20px"
-                  :start-val="0"
-                  :end-val="totalSate.todo"
-                  :duration="2600"
-                />
-              </div>
-              <el-divider direction="vertical" border-style="dashed" />
-              <div class="px-8px text-right">
-                <div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
-                <CountTo
-                  class="text-20px"
-                  :start-val="0"
-                  :end-val="totalSate.access"
-                  :duration="2600"
-                />
-              </div>
-            </div>
-          </el-col>
         </el-row>
       </el-skeleton>
     </el-card>
   </div>
-
-  <el-row class="mt-8px" :gutter="8" justify="space-between">
-    <el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
-      <el-card shadow="never">
-        <template #header>
-          <div class="h-3 flex justify-between">
-            <span>{{ t('workplace.project') }}</span>
-            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
-          </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <el-row>
-            <el-col
-              v-for="(item, index) in projects"
-              :key="`card-${index}`"
-              :xl="8"
-              :lg="8"
-              :md="8"
-              :sm="24"
-              :xs="24"
-            >
-              <el-card shadow="hover">
-                <div class="flex items-center">
-                  <Icon :icon="item.icon" :size="25" class="mr-8px" />
-                  <span class="text-16px">{{ item.name }}</span>
-                </div>
-                <div class="mt-16px text-14px text-gray-400">{{ t(item.message) }}</div>
-                <div class="mt-16px flex justify-between text-12px text-gray-400">
-                  <span>{{ item.personal }}</span>
-                  <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
-                </div>
-              </el-card>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
-
-      <el-card shadow="never" class="mt-8px">
-        <el-skeleton :loading="loading" animated>
-          <el-row :gutter="20" justify="space-between">
-            <el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
-              <el-card shadow="hover" class="mb-8px">
-                <el-skeleton :loading="loading" animated>
-                  <Echart :options="pieOptionsData" :height="280" />
-                </el-skeleton>
-              </el-card>
-            </el-col>
-            <el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
-              <el-card shadow="hover" class="mb-8px">
-                <el-skeleton :loading="loading" animated>
-                  <Echart :options="barOptionsData" :height="280" />
-                </el-skeleton>
-              </el-card>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
-    </el-col>
-    <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
-      <el-card shadow="never">
-        <template #header>
-          <div class="h-3 flex justify-between">
-            <span>{{ t('workplace.shortcutOperation') }}</span>
-          </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <el-row>
-            <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
-              <div class="flex items-center">
-                <Icon :icon="item.icon" class="mr-8px" />
-                <el-link type="default" :underline="false" @click="setWatermark(item.name)">
-                  {{ item.name }}
-                </el-link>
-              </div>
-            </el-col>
-          </el-row>
-        </el-skeleton>
-      </el-card>
-      <el-card shadow="never" class="mt-8px">
-        <template #header>
-          <div class="h-3 flex justify-between">
-            <span>{{ t('workplace.notice') }}</span>
-            <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
-          </div>
-        </template>
-        <el-skeleton :loading="loading" animated>
-          <div v-for="(item, index) in notice" :key="`dynamics-${index}`">
-            <div class="flex items-center">
-              <el-avatar :src="avatar" :size="35" class="mr-16px">
-                <img src="@/assets/imgs/avatar.gif" alt="" />
-              </el-avatar>
-              <div>
-                <div class="text-14px">
-                  <Highlight :keys="item.keys.map((v) => t(v))">
-                    {{ item.type }} : {{ item.title }}
-                  </Highlight>
-                </div>
-                <div class="mt-16px text-12px text-gray-400">
-                  {{ formatTime(item.date, 'yyyy-MM-dd') }}
-                </div>
-              </div>
-            </div>
-            <el-divider />
-          </div>
-        </el-skeleton>
-      </el-card>
-    </el-col>
-  </el-row>
+  <div class="flex flex-col">
+    <el-row :gutter="16" class="row">
+      <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+        <ComparisonCard
+          :tag="t('home.today')"
+          :title="t('home.salesVolume')"
+          prefix="¥"
+          ::decimals="2"
+          :value="(orderComparison?.value?.orderPayPrice || 0)"
+          :reference="(orderComparison?.reference?.orderPayPrice || 0)"
+        />
+      </el-col>
+      <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+        <ComparisonCard
+          :tag="t('home.today')"
+          :title="t('home.userVisits')"
+          :value="userComparison?.value?.visitUserCount || 118"
+          :reference="userComparison?.reference?.visitUserCount || 96"
+        />
+      </el-col>
+      <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+        <ComparisonCard
+        :tag="t('home.today')"
+          :title="t('home.orderVolume')"
+          :value="(orderComparison?.value?.orderPayCount || 0)"
+          :reference="(orderComparison?.reference?.orderPayCount || 0)"
+        />
+      </el-col>
+      <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+        <ComparisonCard
+        :tag="t('home.today')"
+        :title="t('home.newUsers')"
+          :value="userComparison?.value?.registerUserCount || 0"
+          :reference="userComparison?.reference?.registerUserCount || 0"
+        />
+      </el-col>
+    </el-row>
+    <el-row :gutter="16" class="row">
+      <el-col :md="12">
+        <!-- 快捷入口 -->
+        <ShortcutCard />
+      </el-col>
+      <el-col :md="12">
+        <!-- 运营数据 -->
+        <OperationDataCard />
+      </el-col>
+    </el-row>
+    <el-row :gutter="16" class="mb-4">
+      <el-col :md="18" :sm="24">
+        <!-- 会员概览 -->
+        <MemberFunnelCard />
+      </el-col>
+      <!-- <el-col :md="6" :sm="24"> -->
+        <!-- 会员终端 -->
+        <!-- <MemberTerminalCard /> -->
+      <!-- </el-col> -->
+    </el-row>
+    <!-- 交易量趋势 -->
+    <TradeTrendCard class="mb-4" />
+    <!-- 会员统计 -->
+    <!-- <MemberStatisticsCard /> -->
+  </div>
 </template>
 <script lang="ts" setup>
-import { set } from 'lodash-es'
-import { EChartsOption } from 'echarts'
-import { formatTime } from '@/utils'
+import * as GuideTradeStatisticsApi from '@/api/guide/statistics/trade'
+import * as GuideMemberStatisticsApi from '@/api/guide/statistics/member'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+import { GuideTradeOrderSummaryRespVO } from '@/api/guide/statistics/trade'
+import { GuideMemberCountRespVO } from '@/api/guide/statistics/member'
+import { fenToYuan } from '@/utils'
+import ComparisonCard from './components/ComparisonCard.vue'
+import MemberStatisticsCard from './components/MemberStatisticsCard.vue'
+import OperationDataCard from './components/OperationDataCard.vue'
+import ShortcutCard from './components/ShortcutCard.vue'
+import TradeTrendCard from './components/TradeTrendCard.vue'
+import MemberTerminalCard from '@/views/guide/statistics/member/components/MemberTerminalCard.vue'
+import MemberFunnelCard from '@/views/guide/statistics/member/components/MemberFunnelCard.vue'
 
 import { useUserStore } from '@/store/modules/user'
-import { useWatermark } from '@/hooks/web/useWatermark'
-import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
-import { pieOptions, barOptions } from './echarts-data'
+import { valueEquals } from 'element-plus'
 
+/** 商城首页 */
 defineOptions({ name: 'Home' })
-
 const { t } = useI18n()
 const userStore = useUserStore()
-const { setWatermark } = useWatermark()
-const loading = ref(true)
 const avatar = userStore.getUser.avatar
 const username = userStore.getUser.nickname
-const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
-// 获取统计数
-let totalSate = reactive<WorkplaceTotal>({
-  project: 0,
-  access: 0,
-  todo: 0
-})
-
-const getCount = async () => {
-  const data = {
-    project: 40,
-    access: 2340,
-    todo: 10
-  }
-  totalSate = Object.assign(totalSate, data)
-}
 
-// 获取项目数
-let projects = reactive<Project[]>([])
-const getProject = async () => {
-  const data = [
-    {
-      name: 'Github',
-      icon: 'akar-icons:github-fill',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    },
-    {
-      name: 'Vue',
-      icon: 'logos:vue',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    },
-    {
-      name: 'Angular',
-      icon: 'logos:angular-icon',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    },
-    {
-      name: 'React',
-      icon: 'logos:react',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    },
-    {
-      name: 'Webpack',
-      icon: 'logos:webpack',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    },
-    {
-      name: 'Vite',
-      icon: 'vscode-icons:file-type-vite',
-      message: 'workplace.introduction',
-      personal: 'Archer',
-      time: new Date()
-    }
-  ]
-  projects = Object.assign(projects, data)
-}
-
-// 获取通知公告
-let notice = reactive<Notice[]>([])
-const getNotice = async () => {
-  const data = [
-    {
-      title: '系统升级版本',
-      type: '通知',
-      keys: ['通知', '升级'],
-      date: new Date()
-    },
-    {
-      title: '系统凌晨维护',
-      type: '公告',
-      keys: ['公告', '维护'],
-      date: new Date()
-    },
-    {
-      title: '系统升级版本',
-      type: '通知',
-      keys: ['通知', '升级'],
-      date: new Date()
-    },
-    {
-      title: '系统凌晨维护',
-      type: '公告',
-      keys: ['公告', '维护'],
-      date: new Date()
-    }
-  ]
-  notice = Object.assign(notice, data)
-}
+const loading = ref(true) // 加载中
+const orderComparison = ref<GuideDataComparisonRespVO<GuideTradeOrderSummaryRespVO>>() // 交易对照数据
+const userComparison = ref<GuideDataComparisonRespVO<GuideMemberCountRespVO>>() // 用户对照数据
 
-// 获取快捷入口
-let shortcut = reactive<Shortcut[]>([])
 
-const getShortcut = async () => {
-  const data = [
-    {
-      name: 'Github',
-      icon: 'akar-icons:github-fill',
-      url: 'github.io'
-    },
-    {
-      name: 'Vue',
-      icon: 'logos:vue',
-      url: 'vuejs.org'
-    },
-    {
-      name: 'Vite',
-      icon: 'vscode-icons:file-type-vite',
-      url: 'https://vitejs.dev/'
-    },
-    {
-      name: 'Angular',
-      icon: 'logos:angular-icon',
-      url: 'github.io'
-    },
-    {
-      name: 'React',
-      icon: 'logos:react',
-      url: 'github.io'
-    },
-    {
-      name: 'Webpack',
-      icon: 'logos:webpack',
-      url: 'github.io'
-    }
-  ]
-  shortcut = Object.assign(shortcut, data)
+/** 查询交易对照卡片数据 */
+const getOrderComparison = async () => {
+  orderComparison.value = await GuideTradeStatisticsApi.getOrderComparison()
 }
 
-// 用户来源
-const getUserAccessSource = async () => {
-  const data = [
-    { value: 335, name: 'analysis.directAccess' },
-    { value: 310, name: 'analysis.mailMarketing' },
-    { value: 234, name: 'analysis.allianceAdvertising' },
-    { value: 135, name: 'analysis.videoAdvertising' },
-    { value: 1548, name: 'analysis.searchEngines' }
-  ]
-  set(
-    pieOptionsData,
-    'legend.data',
-    data.map((v) => t(v.name))
-  )
-  pieOptionsData!.series![0].data = data.map((v) => {
-    return {
-      name: t(v.name),
-      value: v.value
-    }
-  })
+/** 查询会员用户数量对照卡片数据 */
+const getUserCountComparison = async () => {
+  userComparison.value = await GuideMemberStatisticsApi.getUserCountComparison()
+  //暂时用随机数字表示,稍后会用ApiAccessLog的数据代替。
 }
-const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
 
-// 周活跃量
-const getWeeklyUserActivity = async () => {
-  const data = [
-    { value: 13253, name: 'analysis.monday' },
-    { value: 34235, name: 'analysis.tuesday' },
-    { value: 26321, name: 'analysis.wednesday' },
-    { value: 12340, name: 'analysis.thursday' },
-    { value: 24643, name: 'analysis.friday' },
-    { value: 1322, name: 'analysis.saturday' },
-    { value: 1324, name: 'analysis.sunday' }
-  ]
-  set(
-    barOptionsData,
-    'xAxis.data',
-    data.map((v) => t(v.name))
-  )
-  set(barOptionsData, 'series', [
-    {
-      name: t('analysis.activeQuantity'),
-      data: data.map((v) => v.value),
-      type: 'bar'
-    }
-  ])
+function getRandomNumber(min, max) {
+    return Math.floor(Math.random() * (max - min + 1)) + min;
 }
 
-const getAllApi = async () => {
-  await Promise.all([
-    getCount(),
-    getProject(),
-    getNotice(),
-    getShortcut(),
-    getUserAccessSource(),
-    getWeeklyUserActivity()
-  ])
+/** 初始化 **/
+onMounted(async () => {
+  loading.value = true
+  await Promise.all([getOrderComparison(), getUserCountComparison()])
   loading.value = false
-}
-
-getAllApi()
+})
 </script>
+<style lang="scss" scoped>
+.row {
+  .el-col {
+    margin-bottom: 1rem;
+  }
+}
+</style>

+ 42 - 0
src/views/Home/components/ComparisonCard.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="flex flex-col gap-2 bg-[var(--el-bg-color-overlay)] p-6">
+    <div class="flex items-center justify-between text-gray-500">
+      <span>{{ title }}</span>
+      <el-tag>{{ tag }}</el-tag>
+    </div>
+    <div class="flex flex-row items-baseline justify-between">
+      <CountTo :prefix="prefix" :end-val="value" :decimals="decimals" class="text-3xl" />
+      <span :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'">
+        {{ Math.abs(toNumber(percent)) }}%
+        <Icon :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'" class="!text-sm" />
+      </span>
+    </div>
+    <el-divider class="mb-1! mt-2!" />
+    <div class="flex flex-row items-center justify-between text-sm">
+      <span class="text-gray-500">{{ t('home.yesterdayData') }}</span>
+      <span>{{ prefix || '' }}{{ reference }}</span>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { toNumber } from 'lodash-es'
+import { calculateRelativeRate } from '@/utils'
+const { t } = useI18n()
+/** 交易对照卡片 */
+defineOptions({ name: 'ComparisonCard' })
+
+const props = defineProps({
+  title: propTypes.string.def('').isRequired,
+  tag: propTypes.string.def(''),
+  prefix: propTypes.string.def(''),
+  value: propTypes.number.def(0).isRequired,
+  reference: propTypes.number.def(0).isRequired,
+  decimals: propTypes.number.def(0)
+})
+
+// 计算环比
+const percent = computed(() =>
+  calculateRelativeRate(props.value as number, props.reference as number)
+)
+</script>

+ 91 - 0
src/views/Home/components/MemberStatisticsCard.vue

@@ -0,0 +1,91 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <CardTitle title="用户统计" />
+    </template>
+    <!-- 折线图 -->
+    <Echart :height="300" :options="lineChartOptions" />
+  </el-card>
+</template>
+<script lang="ts" setup>
+import dayjs from 'dayjs'
+import { EChartsOption } from 'echarts'
+import * as MemberStatisticsApi from '@/api/mall/statistics/member'
+import { formatDate } from '@/utils/formatTime'
+import { CardTitle } from '@/components/Card'
+
+/** 会员用户统计卡片 */
+defineOptions({ name: 'MemberStatisticsCard' })
+
+const loading = ref(true) // 加载中
+/** 折线图配置 */
+const lineChartOptions = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['date', 'count'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [{ name: '注册量', type: 'line', smooth: true, areaStyle: {} }],
+  toolbox: {
+    feature: {
+      // 数据区域缩放
+      dataZoom: {
+        yAxisIndex: false // Y轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '会员统计' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: false,
+    axisTick: {
+      show: false
+    },
+    axisLabel: {
+      formatter: (date: string) => formatDate(date, 'MM-DD')
+    }
+  },
+  yAxis: {
+    axisTick: {
+      show: false
+    }
+  }
+}) as EChartsOption
+
+const getMemberRegisterCountList = async () => {
+  loading.value = true
+  // 查询最近一月数据
+  const beginTime = dayjs().subtract(30, 'd').startOf('d')
+  const endTime = dayjs().endOf('d')
+  const list = await MemberStatisticsApi.getMemberRegisterCountList(beginTime, endTime)
+  // 更新 Echarts 数据
+  if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+    lineChartOptions.dataset['source'] = list
+  }
+  loading.value = false
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getMemberRegisterCountList()
+})
+</script>

+ 104 - 0
src/views/Home/components/OperationDataCard.vue

@@ -0,0 +1,104 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <CardTitle :title="t('home.operationalData')" />
+    </template>
+    <div class="flex flex-row flex-wrap items-center gap-8 p-4">
+      <div
+        v-for="item in data"
+        :key="item.name"
+        class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2"
+        @click="handleClick(item.routerName)"
+      >
+        <CountTo
+          :prefix="item.prefix"
+          :end-val="item.value"
+          :decimals="item.decimals"
+          class="text-3xl"
+        />
+        <span class="text-center">{{ item.name }}</span>
+      </div>
+    </div>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import  {SightsApi} from '@/api/guide/sights'
+import {TripApi} from '@/api/guide/trip'
+import  {UsersApi} from '@/api/guide/users'
+import * as GuideTradeStatisticsApi from '@/api/guide/statistics/trade'
+// import * as GuidePayStatisticsApi from '@/api/guide/statistics/pay'
+import { CardTitle } from '@/components/Card'
+const { t } = useI18n()
+/** 运营数据卡片 */
+defineOptions({ name: 'OperationDataCard' })
+
+const router = useRouter() // 路由
+
+/** 数据 */
+const data = reactive({
+  orderUndelivered: { name: t('home.completedPayments'), value: 9, routerName: 'GuideTradeOrder' },
+  orderAfterSaleApply: { name: t('home.refundingOrders'), value: 4, routerName: 'GuideTradeOrder' },
+  orderWaitePickUp: { name: t('home.completedTrips'), value: 0, routerName: 'GuideTradeOrder' },
+  // productAlertStock: { name: '库存预警', value: 0, routerName: 'ProductSpu' },
+  sights: { name: t('home.attractions'), value: 0, routerName: 'Sights' },
+  trip: { name: t('home.itinerary'), value: 0, routerName: 'Trip' },
+  guide: { name: t('home.registerGuide'), value: 0, routerName: 'Users' },
+  // rechargePrice: {
+  //   name: '账户充值',
+  //   value: 0.0,
+  //   prefix: '¥',
+  //   decimals: 2,
+  //   routerName: 'PayWalletRecharge'
+  // }
+})
+
+/** 查询订单数据 */
+const getOrderData = async () => {
+  const orderCount = await GuideTradeStatisticsApi.getOrderCount()
+  data.orderUndelivered.value = orderCount.undelivered
+  data.orderAfterSaleApply.value = orderCount.afterSaleApply
+  data.orderWaitePickUp.value = orderCount.pickUp
+  // data.withdrawAuditing.value = orderCount.auditingWithdraw
+}
+
+/** 查询商品数据 */
+const getSightsData = async () => {
+  // TODO: @芋艿:这个接口的返回值,是不是用命名字段更好些?
+  const SightsCount = await SightsApi.getSightsCount()
+  data.sights.value = SightsCount;
+}
+const getTripData = async () => {
+  // TODO: @芋艿:这个接口的返回值,是不是用命名字段更好些?
+  const TripCount = await TripApi.getTripCount();
+  data.trip.value = TripCount
+}
+
+const getGuideData = async () => {
+  // TODO: @芋艿:这个接口的返回值,是不是用命名字段更好些?
+  const GuideCount = await UsersApi.getGuideCount();
+  data.guide.value = GuideCount
+}
+
+/** 查询钱包充值数据 */
+// const getWalletRechargeData = async () => {
+//   const paySummary = await GuidePayStatisticsApi.getWalletRechargePrice()
+//   data.rechargePrice.value = paySummary.rechargePrice
+// }
+
+/**
+ * 跳转到对应页面
+ *
+ * @param routerName 路由页面组件的名称
+ */
+const handleClick = (routerName: string) => {
+  router.push({ name: routerName })
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getOrderData()
+  getSightsData()
+  getTripData()
+  getGuideData()
+})
+</script>

+ 84 - 0
src/views/Home/components/ShortcutCard.vue

@@ -0,0 +1,84 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <CardTitle :title="t('home.quickAccess')" />
+    </template>
+    <div class="flex flex-row flex-wrap gap-8 p-4">
+      <div
+        v-for="menu in menuList"
+        :key="menu.name"
+        class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2"
+        @click="handleMenuClick(menu.routerName)"
+      >
+        <div
+          :class="menu.bgColor"
+          class="h-48px w-48px flex items-center justify-center rounded text-white"
+        >
+          <Icon :icon="menu.icon" class="text-7.5!" />
+        </div>
+        <span>{{ menu.name }}</span>
+      </div>
+    </div>
+  </el-card>
+</template>
+<script lang="ts" setup>
+/** 快捷入口卡片 */
+import { CardTitle } from '@/components/Card'
+
+defineOptions({ name: 'ShortcutCard' })
+const { t } = useI18n()
+const router = useRouter() // 路由
+
+/** 菜单列表 */
+const menuList = [
+  { name:  t('home.attractions'), icon: 'ep:user-filled', bgColor: 'bg-red-400', routerName: 'MemberUser' },
+  {
+    name: t('home.userManager'),
+    icon: 'fluent-mdl2:product',
+    bgColor: 'bg-orange-400',
+    routerName: 'Sights'
+  },
+  {
+    name: t('home.itineraryManager'),
+    icon: 'fa-solid:project-diagram',
+    bgColor: 'bg-cyan-500',
+    routerName: 'Trip'
+  },
+
+  {
+    name: t('home.guideManager'),
+    icon: 'ri:refund-2-line',
+    bgColor: 'bg-green-600',
+    routerName: 'Users'
+  },
+
+  {
+    name: t('home.orderManager'),
+    icon: 'ep:ticket',
+    bgColor: 'bg-blue-500',
+    routerName: 'GuideTradeOrder'
+  },
+  {
+    name: t('home.paymentManager'),
+    icon: 'fa:group',
+    bgColor: 'bg-purple-500',
+    routerName: 'PayOrder'
+  },
+  // { name: '订单管理', icon: 'ep:list', bgColor: 'bg-yellow-500', routerName: 'GuideTradeOrder' },
+  {
+    name: t('home.refundProcessing'),
+    icon: 'vaadin:money-withdraw',
+    bgColor: 'bg-rose-500',
+    routerName: 'PayRefund'
+  }
+]
+
+/**
+ * 跳转到菜单对应页面
+ *
+ * @param routerName 路由页面组件的名称
+ */
+const handleMenuClick = (routerName: string) => {
+  router.push({ name: routerName })
+}
+</script>

+ 208 - 0
src/views/Home/components/TradeTrendCard.vue

@@ -0,0 +1,208 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="flex flex-row items-center justify-between">
+        <CardTitle :title="t('home.transactionTrend')" />
+        <!-- 查询条件 -->
+        <div class="flex flex-row items-center gap-2">
+          <el-radio-group v-model="timeRangeType" @change="handleTimeRangeTypeChange">
+            <el-radio-button v-for="[key, value] in timeRange.entries()" :key="key" :label="key">
+              {{ value.name }}
+            </el-radio-button>
+          </el-radio-group>
+        </div>
+      </div>
+    </template>
+    <!-- 折线图 -->
+    <Echart :height="300" :options="eChartOptions" />
+  </el-card>
+</template>
+<script lang="ts" setup>
+import dayjs, { Dayjs } from 'dayjs'
+import { EChartsOption } from 'echarts'
+import * as GuideTradeStatisticsApi from '@/api/guide/statistics/trade'
+import { fenToYuan } from '@/utils'
+import { formatDate } from '@/utils/formatTime'
+import { CardTitle } from '@/components/Card'
+
+/** 交易量趋势 */
+defineOptions({ name: 'TradeTrendCard' })
+const { t } = useI18n()
+enum TimeRangeTypeEnum {
+  DAY30 = 1,
+  WEEK = 7,
+  MONTH = 30,
+  YEAR = 365
+} // 日期类型
+const timeRangeType = ref(TimeRangeTypeEnum.DAY30) // 日期快捷选择按钮, 默认30天
+const loading = ref(true) // 加载中
+// 时间范围 Map
+const timeRange = new Map()
+  .set(TimeRangeTypeEnum.DAY30, {
+    name: t('home.30days'),
+    series: [
+      { name: t('home.orderAmount'), type: 'bar', smooth: true, data: [] },
+      { name: t('home.orderQuantity'), type: 'line', smooth: true, data: [] }
+    ]
+  })
+  .set(TimeRangeTypeEnum.WEEK, {
+    name: t('home.week'),
+    series: [
+      { name: t('home.lastWeekAmount'), type: 'bar', smooth: true, data: [] },
+      { name: t('home.thisWeekAmount'), type: 'bar', smooth: true, data: [] },
+      { name: t('home.lastWeekQuantity'), type: 'line', smooth: true, data: [] },
+      { name: t('home.thisWeekQuantity'), type: 'line', smooth: true, data: [] }
+    ]
+  })
+  .set(TimeRangeTypeEnum.MONTH, {
+    name: t('home.month'),
+    series: [
+      { name: t('home.lastMonthAmount'), type: 'bar', smooth: true, data: [] },
+      { name: t('home.thisMonthAmount'), type: 'bar', smooth: true, data: [] },
+      { name: t('home.lastMonthQuantity'), type: 'line', smooth: true, data: [] },
+      { name: t('home.thisMonthQuantity'), type: 'line', smooth: true, data: [] }
+    ]
+  })
+  .set(TimeRangeTypeEnum.YEAR, {
+    name: t('home.year'),
+    series: [
+      { name: t('home.lastYearAmount'), type: 'bar', smooth: true, data: [] },
+      { name: t('home.thisYearAmount'), type: 'bar', smooth: true, data: [] },
+      { name: t('home.lastYearQuantity'), type: 'line', smooth: true, data: [] },
+      { name: t('home.thisYearQuantity'), type: 'line', smooth: true, data: [] }
+    ]
+  })
+/** 图表配置 */
+const eChartOptions = reactive<EChartsOption>({
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  legend: {
+    top: 50,
+    data: []
+  },
+  series: [],
+  toolbox: {
+    feature: {
+      // 数据区域缩放
+      dataZoom: {
+        yAxisIndex: false // Y轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: t('home.transactionTrend') } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  xAxis: {
+    type: 'category',
+    inverse: true,
+    boundaryGap: false,
+    axisTick: {
+      show: false
+    },
+    data: [],
+    axisLabel: {
+      formatter: (date: string) => {
+        switch (timeRangeType.value) {
+          case TimeRangeTypeEnum.DAY30:
+            return formatDate(date, 'MM-DD')
+          case TimeRangeTypeEnum.WEEK:
+            let weekDay = formatDate(date, 'ddd')
+            if (weekDay == '0') weekDay = '日'
+            return '周' + weekDay
+          case TimeRangeTypeEnum.MONTH:
+            return formatDate(date, 'D')
+          case TimeRangeTypeEnum.YEAR:
+            return formatDate(date, 'M') + '月'
+          default:
+            return date
+        }
+      }
+    }
+  },
+  yAxis: {
+    axisTick: {
+      show: false
+    }
+  }
+}) as EChartsOption
+
+/** 时间范围类型单选按钮选中 */
+const handleTimeRangeTypeChange = async () => {
+  // 设置时间范围
+  let beginTime: Dayjs
+  let endTime: Dayjs
+  switch (timeRangeType.value) {
+    case TimeRangeTypeEnum.WEEK:
+      beginTime = dayjs().startOf('week')
+      endTime = dayjs().endOf('week')
+      break
+    case TimeRangeTypeEnum.MONTH:
+      beginTime = dayjs().startOf('month')
+      endTime = dayjs().endOf('month')
+      break
+    case TimeRangeTypeEnum.YEAR:
+      beginTime = dayjs().startOf('year')
+      endTime = dayjs().endOf('year')
+      break
+    case TimeRangeTypeEnum.DAY30:
+    default:
+      beginTime = dayjs().subtract(30, 'day').startOf('d')
+      endTime = dayjs().endOf('d')
+      break
+  }
+  // 发送时间范围选中事件
+  await getOrderCountTrendComparison(beginTime, endTime)
+}
+
+/** 查询订单数量趋势对照数据 */
+const getOrderCountTrendComparison = async (
+  beginTime: dayjs.ConfigType,
+  endTime: dayjs.ConfigType
+) => {
+  loading.value = true
+  // 查询数据
+  const list = await GuideTradeStatisticsApi.getOrderCountTrendComparison(
+    timeRangeType.value,
+    beginTime,
+    endTime
+  )
+  // 处理数据
+  const dates: string[] = []
+  const series = [...timeRange.get(timeRangeType.value).series]
+  for (let item of list) {
+    dates.push(item.value.date)
+    if (series.length === 2) {
+      series[0].data.push(fenToYuan(item?.value?.orderPayPrice || 0)) // 当前金额
+      series[1].data.push(fenToYuan(item?.value?.orderPayCount || 0)) // 当前数量
+    } else {
+      series[0].data.push(fenToYuan(item?.reference?.orderPayPrice || 0)) // 对照金额
+      series[1].data.push(fenToYuan(item?.value?.orderPayPrice || 0)) // 当前金额
+      series[2].data.push(item?.reference?.orderPayCount || 0) // 对照数量
+      series[3].data.push(item?.value?.orderPayCount || 0) // 当前数量
+    }
+  }
+  eChartOptions.xAxis!['data'] = dates
+  eChartOptions.series = series
+  // legend在4个切换到2个的时候,还是显示成4个,需要手动配置一下
+  eChartOptions.legend['data'] = series.map((item) => item.name)
+  loading.value = false
+}
+
+/** 初始化 **/
+onMounted(() => {
+  handleTimeRangeTypeChange()
+})
+</script>

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

@@ -9,8 +9,8 @@
       >
         <!-- 左上角的 logo + 系统标题 -->
         <div class="relative flex items-center text-white">
-          <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
-          <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
+          <img alt="" class="mr-10px h-58px w-58px" src="@/assets/imgs/image-panda-removebg-preview.png" />
+          <span class="text-20px font-bold">{{ underlineToHump(t('common.systemName')) }}</span>
         </div>
         <!-- 左边的背景图 + 欢迎语 -->
         <div class="h-[calc(100%-60px)] flex items-center justify-center">
@@ -19,11 +19,11 @@
             enter-active-class="animate__animated animate__bounceInLeft"
             tag="div"
           >
-            <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />
-            <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
-            <div key="3" class="mt-5 text-14px font-normal text-white">
+            <img key="1" alt="" class="w-450px" src="@/assets/svgs/login-box-bg.svg" />
+            <!-- <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div> -->
+            <!-- <div key="3" class="mt-5 text-14px font-normal text-white">
               {{ t('login.message') }}
-            </div>
+            </div> -->
           </TransitionGroup>
         </div>
       </div>
@@ -51,11 +51,11 @@
             <!-- 手机登录 -->
             <MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
             <!-- 二维码登录 -->
-            <QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+            <!-- <QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> -->
             <!-- 注册 -->
-            <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+            <!-- <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> -->
             <!-- 三方登录 -->
-            <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+            <!-- <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> -->
           </div>
         </Transition>
       </div>

+ 10 - 10
src/views/Login/components/LoginForm.vue

@@ -15,7 +15,7 @@
           <LoginFormTitle style="width: 100%" />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
         <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
           <el-input
             v-model="loginData.loginForm.tenantName"
@@ -25,7 +25,7 @@
             type="primary"
           />
         </el-form-item>
-      </el-col>
+      </el-col> -->
       <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
         <el-form-item prop="username">
           <el-input
@@ -85,31 +85,31 @@
       <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
         <el-form-item>
           <el-row :gutter="5" justify="space-between" style="width: 100%">
-            <el-col :span="8">
+            <el-col :span="24">
               <XButton
                 :title="t('login.btnMobile')"
                 class="w-[100%]"
                 @click="setLoginState(LoginStateEnum.MOBILE)"
               />
             </el-col>
-            <el-col :span="8">
+            <!-- <el-col :span="8">
               <XButton
                 :title="t('login.btnQRCode')"
                 class="w-[100%]"
                 @click="setLoginState(LoginStateEnum.QR_CODE)"
               />
-            </el-col>
-            <el-col :span="8">
+            </el-col> -->
+            <!-- <el-col :span="12">
               <XButton
                 :title="t('login.btnRegister')"
                 class="w-[100%]"
                 @click="setLoginState(LoginStateEnum.REGISTER)"
               />
-            </el-col>
+            </el-col> -->
           </el-row>
         </el-form-item>
       </el-col>
-      <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
+      <!-- <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
       <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
         <el-form-item>
           <div class="w-[100%] flex justify-between">
@@ -139,7 +139,7 @@
             </el-link>
           </div>
         </el-form-item>
-      </el-col>
+      </el-col> -->
     </el-row>
   </el-form>
 </template>
@@ -175,7 +175,7 @@ const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文
 const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
 
 const LoginRules = {
-  tenantName: [required],
+  // tenantName: [required],
   username: [required],
   password: [required]
 }

+ 2 - 2
src/views/Login/components/MobileForm.vue

@@ -16,7 +16,7 @@
           <LoginFormTitle style="width: 100%" />
         </el-form-item>
       </el-col>
-      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+      <!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
         <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
           <el-input
             v-model="loginData.loginForm.tenantName"
@@ -26,7 +26,7 @@
             link
           />
         </el-form-item>
-      </el-col>
+      </el-col> -->
       <!-- 手机号 -->
       <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
         <el-form-item prop="mobileNumber">

+ 276 - 0
src/views/guide/profile/components/BasicInfo.vue

@@ -0,0 +1,276 @@
+<template>
+  <Form ref="formRef" :labelWidth="120" :rules="rules" :schema="schema">
+    <!-- <el-form ref="formRef" :model="schema" :rules="rules" :label-width="200">
+      <el-form-item :label="t('profile.password.oldPassword')" prop="languageType">
+      <InputPassword v-model="schema.languageType" />
+    </el-form-item> -->
+
+    <template #languageType="form">
+      <el-select v-model="form['languageType']" :placeholder="t('profile.info.languageUsed')">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.I8N_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+    </template>
+    <template #selfIntroduction="form">
+      <QEditor v-model="form['selfIntroduction']"  :min-height="250" />
+    </template>
+    <template #categoryIds="form">
+      <SightsCategorySelect v-model="form['categoryIds']" />
+    </template>
+    <template #serviceItems="form">
+      <el-input v-model="form['serviceItems']"  :rows="4" type="textarea" :placeholder="t('profile.info.serviceItems')" />
+    </template>
+    <template #memo="form">
+      <el-input v-model="form['memo']"  :rows="4" type="textarea" :placeholder="t('profile.info.precautions')" />
+    </template>
+    <template #price="form">
+      <el-input
+        v-model="form['price']" 
+        laceholder="t('profile.info.servicePrice')"
+        style="width: 240px" 
+        :formatter="(value) => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
+        :parser="(value) => value.replace(/\$\s?|(,*)/g, '')"/>日元
+    </template>
+    <template #maximumNumber="form">
+      <el-input-number :min="1" :max="10"  style="width: 240px" v-model="form['maximumNumber']" :placeholder="t('profile.info.maxParticipants')" />
+    </template>
+    <template #geographicalIds="form">
+      <el-cascader v-model="form['geographicalIds']" :options="areaList" :props="defaultProps" style="width: 100%"/>
+    </template>
+    <template #startDate="form">
+      <el-date-picker
+          v-model="form['startDate']"
+          type="date"
+          value-format="x"
+          :placeholder="t('profile.info.serviceStartDate')" 
+        />
+    </template>
+    <template #endDate="form">
+      <el-date-picker
+          v-model="form['endDate']"
+          type="date"
+          value-format="x"
+          :placeholder="t('profile.info.serviceEndDate')"
+        />
+    </template>
+    <template #bankInfo="form">
+      <el-input v-model="form['bankInfo']"  :rows="2" type="textarea" :placeholder="t('profile.info.settlementBankInfo')" />
+    </template>
+    <template #picUrls="form">
+      <UploadImgs v-model="form['picUrls']" />
+      <el-divider />
+      <span>
+          为了方便客户及时和您联络,请上传您常用的SNS社交软件用的QR图,客户会随时通过社交软件和您取得联系。
+        </span>
+    </template>
+    <template #wechat="form">
+      <UploadImg v-model="form['wechat']" />
+    </template>
+    <template #facebook="form">
+      <UploadImg v-model="form['facebook']" />
+    </template>
+    <template #whatsapp="form">
+      <UploadImg v-model="form['whatsapp']" />
+    </template>
+    <template #line="form">
+      <UploadImg v-model="form['line']" />
+    </template>
+  </Form>
+  <div style="text-align: center">
+    <XButton :title="t('common.save')" type="primary" @click="submit()" />
+    <XButton :title="t('common.reset')" type="danger" @click="init()" />
+  </div>
+</template>
+<script lang="ts" setup>
+import type { FormRules } from 'element-plus'
+// import { QuillEditor } from '@vueup/vue-quill';
+// import  QuillEditor from './QuillEditor.vue';
+// import '@vueup/vue-quill/dist/vue-quill.snow.css';
+import { FormSchema } from '@/types/form'
+import type { FormExpose } from '@/components/Form'
+import {
+  getUserProfile,
+  updateUserProfileGuideInfo,
+  UserProfileUpdateGuideInfoReqVO
+} from '@/api/system/user/profile'
+import { useUserStore } from '@/store/modules/user'
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import SightsCategorySelect from '@/views/guide/sights/components/SightsCategorySelect.vue'
+import { getAreaTree } from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+
+defineOptions({ name: 'GuideBasicInfo' })
+
+const { t } = useI18n()
+const message = useMessage() // 消息弹窗
+// const userStore = useUserStore() 
+// 表单校验
+const rules = reactive<FormRules>({
+})
+const schema = reactive<FormSchema[]>([
+  {
+    label: t('profile.info.languageUsed'),
+    field: 'languageType',
+    dictType: DICT_TYPE.I8N_TYPE,
+    dictClass: 'number',
+    isTable: false,
+    form: {
+      component: 'SelectV2'
+    }
+  },
+  {
+    label: t('profile.info.textAndImageIntro'),
+    field: 'selfIntroduction',
+    isTable: false,
+    form: {
+      component: 'Editor',
+      componentProps: {
+        valueHtml: '',
+        height: 200
+      }
+    }
+  },
+  {
+    label: t('profile.info.expertiseField'),
+    field: 'categoryIds',
+    isTable: false,
+    form: {
+      component: 'SightsCategorySelect'
+    }
+  },
+  {
+    label: t('profile.info.serviceItems'),
+    field: 'serviceItems',
+    isTable: false,
+    form: {
+      component: 'Input',
+      componentProps: {
+        type: 'textarea',
+        rows: 4
+      },
+      colProps: {
+        span: 24
+      }
+    }
+  },
+  {
+    label: t('profile.info.precautions'),
+    field: 'memo',
+    isTable: false,
+    form: {
+      component: 'Input',
+      componentProps: {
+        type: 'textarea',
+        rows: 4
+      },
+      colProps: {
+        span: 24
+      }
+    }
+  },
+  {
+    label: t('profile.info.servicePrice'),
+    field: 'price',
+    isTable: false,
+    form: {
+      component: 'InputNumber',
+      value: 0
+    }
+  },
+  {
+    label: t('profile.info.maxParticipants'),
+    field: 'maximumNumber',
+    isTable: false,
+    form: {
+      component: 'InputNumber',
+      value: 0
+    }
+  },
+  {
+    label: t('profile.info.region'),
+    field: 'geographicalIds',
+    isTable: false,
+    form: {
+      component: 'Checkbox'
+    }
+  },
+  {
+    label: t('profile.info.serviceStartDate'),
+    field: 'startDate',
+    isTable: false,
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'datetime',
+        valueFormat: 'x'
+      }
+    }
+  },
+  {
+    label: t('profile.info.serviceEndDate'),
+    field: 'endDate',
+    isTable: false,
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'datetime',
+        valueFormat: 'x'
+      }
+    }
+  },
+  {
+    label: t('profile.info.settlementBankInfo'),
+    field: 'bankInfo',
+  },
+  {
+    label: t('guide.sight.attractionCarouselImages'),
+    field: 'picUrls',
+  },
+
+  {
+    label: '微信 QR图',
+    field: 'wechat',
+  },
+  {
+    label: 'Facebook QR图',
+    field: 'facebook',
+  },
+  {
+    label: 'Whatsapp QR图',
+    field: 'whatsapp',
+  },
+  {
+    label: 'Line QR图',
+    field: 'line',
+  }
+])
+const formRef = ref<FormExpose>() // 表单 Ref
+const areaList = ref() // 区域树
+
+const submit = () => {
+  const elForm = unref(formRef)?.getElFormRef()
+  if (!elForm) return
+  elForm.validate(async (valid) => {
+    if (valid) {
+      const data = unref(formRef)?.formModel as UserProfileUpdateGuideInfoReqVO
+      await updateUserProfileGuideInfo(data)
+      message.success(t('common.updateSuccess'))
+      // const profile = await init()
+      // userStore.setUserNicknameAction(profile.nickname)
+    }
+  })
+}
+const init = async () => {
+  const res = await getUserProfile()
+  unref(formRef)?.setValues(res)
+  return res
+}
+onMounted(async () => {
+  areaList.value = await getAreaTree()
+  await init()
+})
+</script>

+ 256 - 0
src/views/guide/profile/components/Calendar.vue

@@ -0,0 +1,256 @@
+<template>
+  <div id="app">
+    <vue-cal
+      :events="events"
+      :cell-classes="getCellClass"
+      :from-page="fromPage"
+      :to-page="toPage"
+      default-view="month"
+      :disable-views="['years', 'year', 'week', 'day', 'multi-day']"
+      @cell-click="handleCellClick"
+      class="vuecal--blue-theme"
+      events-on-month-view="short"
+    >
+      <template #event="{ event }">
+        <div
+          v-if="event"
+          class="event-content"
+          :class="getEventClass(event.title)"
+          @click="handleEventClick(event, $event)"
+        >
+          <i :class="getIconClass(event.title)"></i> {{ event.title }}
+        </div>
+      </template>
+    </vue-cal>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import VueCal from 'vue-cal'
+import 'vue-cal/dist/vuecal.css' // 导入样式
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+import { ScheduleApi, ScheduleVO } from '@/api/guide/schedule'
+
+
+const list = ref<ScheduleVO[]>([]) // 列表的数据
+const schedule = ref({
+  id: undefined,
+  guideId: undefined,
+  bookingDate: undefined,
+  bookingAvailability: undefined,
+  booked: undefined
+})
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+interface Event {
+  start: string
+  end: string
+  title: `t('profile.info.work')` | `t('profile.info.rest')` | `t('profile.info.booked')`
+}
+const loading = ref(true) // 列表的加载中
+const today = new Date()
+const todayString = formatDate(today) //.toISOString().split('T')[0];
+const fromPage = new Date(new Date().setFullYear(new Date().getFullYear() - 1))
+const toPage = new Date(new Date().setFullYear(new Date().getFullYear() + 1))
+const events = ref<Event[]>([])
+const userInfo = ref({} as ProfileVO)
+const getUserInfo = async () => {
+  const users = await getUserProfile()
+  userInfo.value = users
+}
+
+const handleCellClick = async (event: any) => {
+// function handleCellClick(event: any) {
+  console.log('Event:', event) // 调试输出 event 对象
+  console.log('formatDate:', formatDate(event))
+  if (!event || !formatDate(event)) {
+    console.error('Invalid event or date:', event)
+    return
+  }
+  // 提取日期部分并创建新的日期对象
+  let selectedDate
+  try {
+    const datePart = formatDate(event)
+    console.log('datePart:', datePart) // 调试输出 event 对象
+    selectedDate = new Date(datePart)
+  } catch (error) {
+    return
+  }
+
+  if (isNaN(selectedDate.getTime())) {
+    return
+  }
+
+  const dateString = selectedDate.toISOString().split('T')[0]
+
+  if (new Date(dateString) < new Date(todayString)) {
+    alert("Cannot modify dates prior to today")
+    return
+  }
+
+  const existingEvent = events.value.find((e) => e.start === dateString)
+
+  if (existingEvent && existingEvent.title === t('profile.info.booked')) {
+    alert("This date has been booked and cannot be modified.")
+    return
+  }
+
+  let newTitle = ''
+  if (existingEvent) {
+    // newTitle = existingEvent.title === '工作' ? '休息' : '工作';
+    existingEvent.title = existingEvent.title === t('profile.info.work') ? t('profile.info.rest') : t('profile.info.work')
+    //todo update scheduled
+    let update_scheduled = list.value.find((e) => formatDate(new Date(e.bookingDate)) === dateString);
+    update_scheduled.bookingAvailability = existingEvent.title ===  t('profile.info.work')? true : false;
+    await ScheduleApi.updateSchedule(update_scheduled)
+    message.success(t('common.updateSuccess'))
+
+  } else {
+    newTitle = t('profile.info.work')
+    events.value.push({
+      start: dateString,
+      end: dateString,
+      title: `t('profile.info.work')`
+    })
+    //todo insert new scheduled
+    schedule.value.booked=false;
+    schedule.value.bookingAvailability=true;
+    schedule.value.guideId=userInfo.value.id;
+    schedule.value.bookingDate=new Date(dateString).getTime();
+    await ScheduleApi.createSchedule(schedule.value);
+    message.success(t('common.createSuccess'))
+  }
+  
+}
+
+function getCellClass({ date }: { date: Date }) {
+  const dateString = formatDate(date) //.toISOString().split('T')[0];
+
+  const existingEvent = events.value.find((e) => e.start === dateString)
+
+  if (existingEvent) {
+    if (existingEvent.title === t('profile.info.booked')) {
+      return 'reserved-date'
+    } else if (existingEvent.title === t('profile.info.work')) {
+      return 'work-date'
+    } else if (existingEvent.title === t('profile.info.rest')) {
+      return 'rest-date'
+    }
+  }
+
+  return new Date(dateString) < new Date(todayString) ? 'past-date' : ''
+}
+function formatDate(dt) {
+  // 打印传入的对象,以便调试
+  console.log('Received dt:', dt)
+
+  // 获取日期对象
+  let date = dt // 假设您想格式化的是开始日期
+
+  if (!(date instanceof Date)) {
+    console.log('date is not a Date object, attempting to create Date...')
+    date = new Date(dt.start)
+  }
+
+  // 检查转换后的date是否有效
+  if (isNaN(date.getTime())) {
+    throw new Error('Invalid date')
+  }
+
+  // 格式化日期
+  const y = date.getFullYear()
+  const m = ('00' + (date.getMonth() + 1)).slice(-2)
+  const d = ('00' + date.getDate()).slice(-2)
+  return `${y}-${m}-${d}`
+}
+function getEventClass(title) {
+  switch (title) {
+    case t('profile.info.work'):
+      return 'work-icon'
+    case t('profile.info.rest'):
+      return 'rest-icon'
+    case t('profile.info.booked'):
+      return 'reserved-icon'
+  }
+}
+
+function getIconClass(title) {
+  switch (title) {
+    case t('profile.info.work'):
+      return 'fas fa-briefcase' // 示例:FontAwesome的图标类
+    case t('profile.info.rest'):
+      return 'fas fa-bed'
+    case t('profile.info.booked'):
+      return 'fas fa-ban'
+  }
+}
+function handleEventClick(eventData, domEvent) {
+  domEvent.stopPropagation() // 阻止事件冒泡到单元格
+  console.log('Event content clicked:', eventData)
+  // 你可以在这里调用 handleCellClick 或者另外处理
+  handleCellClick(eventData)
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const users = await getUserProfile()
+    userInfo.value = users
+    const data = await ScheduleApi.getSchedulePage(userInfo.value.id)
+    list.value = data
+    data.forEach((schedule) => {
+      let eventTitle = schedule.booked ? t('profile.info.booked') : schedule.bookingAvailability ? t('profile.info.work') : t('profile.info.rest')
+      let eventDate = new Date(schedule.bookingDate)
+      let startDate = formatDate(eventDate)
+      events.value.push({
+        start: startDate,
+        end: startDate,
+        title: eventTitle
+      })
+    })
+  } finally {
+    loading.value = false
+  }
+}
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style>
+.past-date {
+  background-color: #f0f0f0 !important;
+  pointer-events: none;
+}
+.work-date {
+  background-color: #a6e1fa !important;
+}
+.rest-date {
+  background-color: #ffd700 !important;
+}
+.reserved-date {
+  background-color: #ff6347 !important;
+  pointer-events: none;
+}
+.event-content {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+}
+
+.work-icon {
+  color: #1e90ff; /* 蓝色 */
+}
+
+.rest-icon {
+  color: #ffd700; /* 黄色 */
+}
+
+.reserved-icon {
+  color: #ff6347; /* 红色 */
+}
+</style>

+ 188 - 0
src/views/guide/profile/components/SelfCommentInfo.vue

@@ -0,0 +1,188 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="120px"
+    v-loading="formLoading"
+  >
+    <!-- <el-form-item label="评价人ID" prop="userId">
+      <el-input
+        v-model="formData.userId"
+        placeholder="请输入评价人的用户编号"
+        style="width: 240px"
+      />
+    </el-form-item>
+    <el-form-item label="评价人" prop="userNickname">
+      <el-input
+        v-model="formData.userNickname"
+        placeholder="请输入评价人名称"
+        style="width: 240px"
+      />
+    </el-form-item>
+    <el-form-item label="类型" prop="type">
+      <el-select v-model="formData.type" placeholder="请选择客户评价/自我评价" style="width: 240px">
+        <el-option
+          v-for="dict in getIntDictOptions(DICT_TYPE.GUIDE_COMMENT_TYPE)"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
+    </el-form-item> -->
+    <el-form-item :label="t('profile.info.ratingStars')" prop="scores">
+      <el-rate v-model="formData.scores" />
+      <!-- <el-input v-model="formData.scores" placeholder="请输入评分星级1-5分" /> -->
+    </el-form-item>
+    <el-form-item :label="t('profile.info.expertiseStars')"  prop="expertiseScores">
+      <el-rate v-model="formData.expertiseScores" />
+      <!-- <el-input v-model="formData.expertiseScores" placeholder="请输入专业知识星级 1-5 星" /> -->
+    </el-form-item>
+    <el-form-item :label="t('profile.info.serviceStars')"  prop="benefitScores">
+      <el-rate v-model="formData.benefitScores" />
+      <!-- <el-input v-model="formData.benefitScores" placeholder="请输入服务星级 1-5 星" /> -->
+    </el-form-item>
+    <el-form-item :label="t('profile.info.reviewContent')"  prop="content">
+      <QEditor v-model="formData.content" :min-height="250" />
+    </el-form-item>
+    <el-form-item :label="t('profile.info.likesCount')" prop="likeCount">
+      <el-input-number v-model="formData.likeCount" :min="1" />
+    </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> -->
+  <div style="text-align: center">
+    <XButton :title="t('common.save')" type="primary" @click="submitForm()" />
+    <XButton :title="t('common.reset')" type="danger" @click="resetForm()" />
+  </div>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { UsersApi } from '@/api/guide/users'
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+
+defineOptions({ name: 'SelfCommentInfo' })
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  userId: undefined,
+  userNickname: undefined,
+  userAvatar: undefined,
+  type: undefined,
+  guideId: undefined,
+  scores: undefined,
+  expertiseScores: undefined,
+  benefitScores: undefined,
+  content: undefined,
+  likeCount: undefined
+})
+const formRules = reactive({
+  scores: [{ required: true, message: t('profile.info.ratingStars')+' ' +t('common.required'), trigger: 'blur' }],
+  expertiseScores: [{ required: true, message: t('profile.info.expertiseStars')+' ' +t('common.required'), trigger: 'blur' }],
+  benefitScores: [{ required: true, message: t('profile.info.serviceStars')+' ' +t('common.required'), trigger: 'blur' }],
+  content: [{ required: true, message: t('profile.info.reviewContent')+' ' +t('common.required'), trigger: 'blur' }]
+  // likeCount: [{ required: true, message: '点赞数不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+// const open = async (type: string, id?: number, guideId: number) => {
+//   dialogVisible.value = true
+//   dialogTitle.value = t('action.' + type)
+//   formType.value = type
+//   resetForm()
+//   formData.value.guideId = guideId
+//   // 修改时,设置数据
+//   if (id) {
+//     formLoading.value = true
+//     try {
+//       formData.value = await UsersApi.getComment(id)
+//     } finally {
+//       formLoading.value = false
+//     }
+//   }
+// }
+// defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+// const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      data.userId = userInfo.value.id
+      data.userNickname = userInfo.value.nickname
+      data.guideId = userInfo.value.id
+      data.type = 1
+      await UsersApi.createComment(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await UsersApi.updateComment(data)
+      message.success(t('common.updateSuccess'))
+    }
+    // dialogVisible.value = false
+    // 发送操作成功的事件
+    // emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    userId: undefined,
+    userNickname: undefined,
+    userAvatar: undefined,
+    type: undefined,
+    guideId: undefined,
+    scores: undefined,
+    expertiseScores: undefined,
+    benefitScores: undefined,
+    content: undefined,
+    likeCount: undefined
+  }
+  formRef.value?.resetFields()
+}
+const init = async () => {
+  // dialogVisible.value = true
+  // dialogTitle.value = t('action.' + type)
+  // formType.value = type
+  resetForm()
+  try {
+    formLoading.value = true
+    const data = await UsersApi.getSelfComment()
+    if (data!=null){ formData.value = data}
+  } finally {
+    formLoading.value = false
+  }
+  // 修改时,设置数据
+  if (formData.value.id == undefined) {
+    formType.value = 'create'
+  } else {
+    formType.value = 'update'
+  }
+}
+const userInfo = ref({} as ProfileVO)
+const getUserInfo = async () => {
+  const users = await getUserProfile()
+  userInfo.value = users
+}
+
+onMounted(async () => {
+  // areaList.value = await getAreaTree()
+  await init()
+  await getUserInfo()
+})
+</script>

+ 76 - 0
src/views/guide/profile/index.vue

@@ -0,0 +1,76 @@
+<template>
+  <div class="flex">
+    <el-card class="user w-1/3" shadow="hover">
+      <template #header>
+        <div class="card-header">
+          <span>{{ t('profile.user.title') }}</span>
+        </div>
+      </template>
+      <ProfileUser />
+    </el-card>
+    <el-card class="user ml-3 w-2/3" shadow="hover">
+      <template #header>
+        <div class="card-header">
+          <span>{{ t('profile.info.title') }}</span>
+        </div>
+      </template>
+      <div>
+        <el-tabs v-model="activeName" class="profile-tabs" style="height: 600px" tab-position="top">
+          <el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo">
+            <GuideBasicInfo />
+          </el-tab-pane>
+          <el-tab-pane :label="t('profile.info.commentInfo')" name="commentInfo">
+            <SelfCommentInfo />
+          </el-tab-pane>
+          <el-tab-pane :label="t('profile.info.resetPwd')" name="resetPwd">
+            <ResetPwd />
+          </el-tab-pane>
+          <el-tab-pane :label="t('profile.info.Schedule')" name="calendar">
+            <Calendar />
+          </el-tab-pane>
+          
+        </el-tabs>
+      </div>
+    </el-card>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ProfileUser, ResetPwd } from '@/views/Profile/components'
+import GuideBasicInfo  from './components/BasicInfo.vue'
+import SelfCommentInfo  from './components/SelfCommentInfo.vue'
+import Calendar from './components/Calendar.vue';
+const { t } = useI18n()
+defineOptions({ name: 'Profile' })
+const activeName = ref('basicInfo')
+</script>
+<style scoped>
+.user {
+  max-height: 960px;
+  padding: 15px 20px 20px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+:deep(.el-card .el-card__header, .el-card .el-card__body) {
+  padding: 15px !important;
+}
+
+.profile-tabs > .el-tabs__content {
+  padding: 32px;
+  font-weight: 600;
+  color: #6b778c;
+}
+
+.el-tabs--left .el-tabs__content {
+  height: 100%;
+  flex-grow: 1;
+  overflow: auto;
+}
+.el-card {
+overflow: auto;
+}
+</style>

+ 132 - 0
src/views/guide/schedule/ScheduleForm.vue

@@ -0,0 +1,132 @@
+<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="guideId">
+        <el-input v-model="formData.guideId" placeholder="请输入导游编号" />
+      </el-form-item>
+      <el-form-item label="预约日期" prop="bookingDate">
+        <el-date-picker
+          v-model="formData.bookingDate"
+          type="date"
+          value-format="x"
+          placeholder="选择预约日期"
+        />
+      </el-form-item>
+      <el-form-item label="是否可以被预约" prop="bookingAvailability">
+        <el-radio-group v-model="formData.bookingAvailability">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="已经被预约否?" prop="booked">
+        <el-radio-group v-model="formData.booked">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </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 { getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ScheduleApi, ScheduleVO } from '@/api/guide/schedule'
+
+/** 预约旅程日程管理情报 表单 */
+defineOptions({ name: 'ScheduleForm' })
+
+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,
+  guideId: undefined,
+  bookingDate: undefined,
+  bookingAvailability: undefined,
+  booked: undefined
+})
+const formRules = reactive({
+  guideId: [{ required: true, message: '导游编号不能为空', trigger: 'blur' }],
+  bookingDate: [{ required: true, message: '预约日期不能为空', trigger: 'blur' }],
+  bookingAvailability: [{ required: true, message: '是否可以被预约不能为空', trigger: 'blur' }],
+  booked: [{ 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 ScheduleApi.getSchedule(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ScheduleVO
+    if (formType.value === 'create') {
+      await ScheduleApi.createSchedule(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ScheduleApi.updateSchedule(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    guideId: undefined,
+    bookingDate: undefined,
+    bookingAvailability: undefined,
+    booked: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 249 - 0
src/views/guide/schedule/index.vue

@@ -0,0 +1,249 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="导游编号" prop="guideId">
+        <el-input
+          v-model="queryParams.guideId"
+          placeholder="请输入导游编号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="预约日期" prop="bookingDate">
+        <el-date-picker
+          v-model="queryParams.bookingDate"
+          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 label="是否可以被预约" prop="bookingAvailability">
+        <el-select
+          v-model="queryParams.bookingAvailability"
+          placeholder="请选择是否可以被预约"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="已经被预约否?" prop="booked">
+        <el-select
+          v-model="queryParams.booked"
+          placeholder="请选择已经被预约否?"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :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"
+          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-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['guide:schedule:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['guide:schedule:export']"
+        >
+          <Icon icon="ep:download" 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" />
+      <el-table-column label="导游编号" align="center" prop="guideId" />
+      <el-table-column
+        label="预约日期"
+        align="center"
+        prop="bookingDate"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="是否可以被预约" align="center" prop="bookingAvailability">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.bookingAvailability" />
+        </template>
+      </el-table-column>
+      <el-table-column label="已经被预约否?" align="center" prop="booked">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.booked" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['guide:schedule:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['guide:schedule:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ScheduleForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { ScheduleApi, ScheduleVO } from '@/api/guide/schedule'
+import ScheduleForm from './ScheduleForm.vue'
+
+/** 预约旅程日程管理情报 列表 */
+defineOptions({ name: 'Schedule' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<ScheduleVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  guideId: undefined,
+  bookingDate: [],
+  bookingAvailability: undefined,
+  booked: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ScheduleApi.getSchedulePage()
+    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 handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ScheduleApi.deleteSchedule(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await ScheduleApi.exportSchedule(queryParams)
+    download.excel(data, '预约旅程日程管理情报.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 265 - 0
src/views/guide/sights/SightsDetail.vue

@@ -0,0 +1,265 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1024px">
+    <el-icon>
+        <ElementPlus />
+    </el-icon>
+    <el-text class="mx-1" size="large">{{ t('guide.sight.baseInfo') }}</el-text>
+    <el-form 
+      ref="formRef"
+      :model="formData"
+      label-width="150px"
+      v-loading="formLoading"
+      disabled="true"
+    >
+      <el-form-item :label="t('guide.sight.attractionCategory')" prop="categoryIds">
+        <SightsCategorySelect v-model="formData.categoryIds" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.attractionCoverImage')" prop="picUrl">
+        <UploadImg v-model="formData.picUrl" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.attractionCarouselImages')" prop="sliderPicUrls">
+        <UploadImgs v-model="formData.sliderPicUrls" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.officialWebsiteURL')" prop="url">
+        <el-input v-model="formData.url" placeholder="请输入官网URL" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.phoneNumber')" prop="tel">
+        <el-input v-model="formData.tel"/>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.openingHours')" prop="opentime">
+        <el-input v-model="formData.opentime" placeholder="请输入电开放时间" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.playTime')" prop="durationSightseeing">
+        <el-input-number v-model="formData.durationSightseeing" :min="1" :max="10"/>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.region')" prop="geographicalIds">
+        <el-cascader v-model="formData.geographicalIds" :options="areaList" :props="defaultProps" />
+      </el-form-item>
+    </el-form>
+    <el-divider />
+    <el-icon>
+        <ElementPlus />
+    </el-icon>
+    <el-text class="mx-1" size="large">{{t('guide.sight.multilingualExpansionInfo')}}</el-text>
+    <el-form 
+      :model="formData.sightsI18nExtensionDOList"
+      label-width="150px"
+      v-loading="formLoading"
+      disabled="true"
+    >
+    <div v-for="(item, index) in formData.sightsI18nExtensionDOList" :key="index">
+      <el-form-item :label="t('guide.sight.multilingualType')" prop="languageType">
+        <el-radio-group v-model="formData.sightsI18nExtensionDOList[index].languageType">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.I8N_TYPE)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.title')" prop="title">
+        <el-input v-model="formData.sightsI18nExtensionDOList[index].title"/>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.subtitle')" prop="subtitle">
+        <el-input v-model="formData.sightsI18nExtensionDOList[index].subtitle" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.introduction')" prop="overview">
+        <el-input v-model="formData.sightsI18nExtensionDOList[index].overview" :rows="3" type="textarea"/>
+
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.attractionTextualDescription')" prop="description">
+        <Editor v-model="formData.sightsI18nExtensionDOList[index].description" height="350px" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.address')" prop="address">
+        <el-input v-model="formData.sightsI18nExtensionDOList[index].address" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.ticketsAndReservations')" prop="bookingDetails">
+        <el-input v-model="formData.sightsI18nExtensionDOList[index].bookingDetails" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.keywordGroup')" prop="keywords">
+        <!-- <el-input v-model="formData.sightsI18nExtensionDOList[index].keywords" placeholder="请输入关键字" /> -->
+        <div class="flex gap-2">
+          <el-tag
+            v-for="tag in dynamicTags"
+            :key="tag"
+            :disable-transitions="false"
+            @close="handleClose(tag)"
+          >
+            {{ tag }}
+          </el-tag>
+          <el-input
+            v-if="inputVisible"
+            ref="InputRef"
+            v-model="inputValue"
+            class="w-20"
+            size="small"
+            @keyup.enter="handleInputConfirm"
+            @blur="handleInputConfirm"
+          />
+        </div>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.additionalInfo')" prop="memo">
+        <el-input :rows="2" v-model="formData.sightsI18nExtensionDOList[index].memo"/>
+      </el-form-item>
+    </div>
+    </el-form>
+
+    <el-divider />
+    <el-icon>
+        <ElementPlus />
+    </el-icon>
+    <el-text class="mx-1" size="large">{{ t('guide.sight.comments') }}</el-text>
+    <el-form
+      :model="formData"
+      label-width="150px"
+      v-loading="formLoading"
+      disabled="true"
+    >
+    <div v-for="(item, index) in formData.sightsCommentDOList" :key="index">
+      <el-form-item :label="t('guide.sight.ratingStars')" prop="scores">
+        <el-rate
+          v-model="formData.sightsCommentDOList[index].scores"
+          :texts="['oops', 'disappointed', 'normal', 'good', 'great']"
+          show-text
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.sceneryStars')" prop="sceneryScores">
+        <!-- <el-input v-model="formData.sceneryScores" placeholder="请输入景色星级 1-5 星" /> -->
+        <el-rate
+          v-model="formData.sightsCommentDOList[index].sceneryScores"
+          :texts="['oops', 'disappointed', 'normal', 'good', 'great']"
+          show-text
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.serviceStars')" prop="benefitScores">
+        <!-- <el-input v-model="formData.benefitScores" placeholder="请输入服务星级 1-5 星" /> -->
+        <el-rate
+          v-model="formData.sightsCommentDOList[index].benefitScores"
+          :texts="['oops', 'disappointed', 'normal', 'good', 'great']"
+          show-text
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.reviewContent')" prop="content">
+        <Editor v-model="formData.sightsCommentDOList[index].content" height="200px" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.tourDuration')" prop="durationTour">
+        <el-input-number v-model="formData.sightsCommentDOList[index].durationTour" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.likesCount')" prop="likeCount">
+        <el-input-number v-model="formData.sightsCommentDOList[index].likeCount"  :min="1" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.image')" prop="picUrls">
+        <!-- <el-input v-model="formData.picUrls" placeholder="请输入评论图片地址数组" /> -->
+        <UploadImgs v-model="formData.sightsCommentDOList[index].picUrls" />
+      </el-form-item>
+    </div>
+    </el-form>
+    <template #footer>
+      <!-- <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> -->
+      <el-button @click="dialogVisible = false">{{t('common.close')}}</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { SightsApi, SightsVO } from '@/api/guide/sights'
+import SightsCategorySelect from '@/views/guide/sights/components/SightsCategorySelect.vue'
+import { getAreaTree } from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+import { ElInput } from 'element-plus'
+import { Bell, ElementPlus } from '@element-plus/icons-vue'
+/** 观光景点基本信息 表单 */
+defineOptions({ name: 'SightsForm' })
+
+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,
+  categoryIds: undefined,
+  picUrl: undefined,
+  sliderPicUrls: undefined,
+  url: undefined,
+  tel: undefined,
+  opentime: undefined,
+  durationSightseeing: undefined,
+  geographicalId: undefined,
+  sort: undefined,
+  status: undefined,
+  sightsI18nExtensionDOList: [undefined],
+  sightsCommentDOList: [undefined]
+})
+const formRef = ref() // 表单 Ref
+const areaList = ref() // 区域树
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.detail')
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await SightsApi.getSights(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    categoryIds: undefined,
+    picUrl: undefined,
+    sliderPicUrls: undefined,
+    url: undefined,
+    tel: undefined,
+    opentime: undefined,
+    durationSightseeing: undefined,
+    geographicalId: undefined,
+    sort: undefined,
+    status: undefined
+  }
+  formRef.value?.resetFields()
+}
+/** 初始化 **/
+onMounted(async () => {
+  areaList.value = await getAreaTree()
+})
+const inputValue = ref('')
+const dynamicTags = ref(['文化', '风景', '历史'])
+const inputVisible = ref(false)
+const InputRef = ref<InstanceType<typeof ElInput>>()
+
+const handleClose = (tag: string) => {
+  dynamicTags.value.splice(dynamicTags.value.indexOf(tag), 1)
+}
+
+const showInput = () => {
+  inputVisible.value = true
+  nextTick(() => {
+    InputRef.value!.input!.focus()
+  })
+}
+
+const handleInputConfirm = () => {
+  if (inputValue.value) {
+    dynamicTags.value.push(inputValue.value)
+  }
+  inputVisible.value = false
+  inputValue.value = ''
+}
+</script>

+ 156 - 0
src/views/guide/sights/SightsForm.vue

@@ -0,0 +1,156 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1024px">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="150px"
+      v-loading="formLoading"
+    >
+
+      <el-form-item :label="t('guide.sight.attractionCategory')" prop="categoryIds">
+        <SightsCategorySelect v-model="formData.categoryIds" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.attractionCoverImage')" prop="picUrl">
+        <UploadImg v-model="formData.picUrl" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.attractionCarouselImages')" prop="sliderPicUrls">
+        <UploadImgs v-model="formData.sliderPicUrls" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.officialWebsiteURL')" prop="url">
+        <el-input v-model="formData.url" :placeholder="t('guide.sight.enterOfficialWebsiteURL')" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.phoneNumber')" prop="tel">
+        <el-input v-model="formData.tel" :placeholder="t('guide.sight.enterPhoneNumber')" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.openingHours')" prop="opentime">
+        <el-input v-model="formData.opentime" :placeholder="t('guide.sight.enterOpeningHours')" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.playTime')" prop="durationSightseeing">
+        <el-input-number v-model="formData.durationSightseeing" :min="1" :max="10" :placeholder="t('guide.sight.enterApproximatePlayTime')" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.region')" prop="geographicalIds">
+        <!-- <el-input v-model="formData.geographicalId" placeholder="请输入地理信息表ID" /> -->
+        <el-cascader v-model="formData.geographicalIds" :options="areaList" :props="defaultProps" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.sort')" prop="sort">
+        <el-input-number v-model="formData.sort" :min="1" :max="10" />
+      </el-form-item>
+      <el-form-item :label="t('guide.category.enableStatus')" 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>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">{{t('guide.common.confirm')}}</el-button>
+      <el-button @click="dialogVisible = false">{{t('guide.common.cancel')}}</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { SightsApi, SightsVO } from '@/api/guide/sights'
+import SightsCategorySelect from '@/views/guide/sights/components/SightsCategorySelect.vue'
+import { getAreaTree } from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+
+/** 观光景点基本信息 表单 */
+defineOptions({ name: 'SightsForm' })
+
+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,
+  categoryIds: undefined,
+  picUrl: undefined,
+  sliderPicUrls: undefined,
+  url: undefined,
+  tel: undefined,
+  opentime: undefined,
+  durationSightseeing: undefined,
+  geographicalId: undefined,
+  sort: undefined,
+  status: undefined
+})
+const formRules = reactive({
+  categoryIds: [{ required: true, message: t('guide.sight.attractionCategory') + ' '+t('common.required'), trigger: 'change' }],
+  status: [{ required: true, message:  t('guide.category.enableStatus') + ' '+t('common.required'), 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 SightsApi.getSights(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as SightsVO
+    if (formType.value === 'create') {
+      await SightsApi.createSights(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await SightsApi.updateSights(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    categoryIds: undefined,
+    picUrl: undefined,
+    sliderPicUrls: undefined,
+    url: undefined,
+    tel: undefined,
+    opentime: undefined,
+    durationSightseeing: undefined,
+    geographicalId: undefined,
+    sort: undefined,
+    status: undefined
+  }
+  formRef.value?.resetFields()
+}
+/** 初始化 **/
+onMounted(async () => {
+  areaList.value = await getAreaTree()
+})
+</script>

+ 42 - 0
src/views/guide/sights/components/SightsCategoryDisplay.vue

@@ -0,0 +1,42 @@
+<template>
+
+      <el-tag v-for="item in categoryList" :key="item.id" size="small">{{ item.name }}<br/></el-tag>
+
+</template>
+<script lang="ts" setup>
+import {SightsCategoryApi,SightsCategoryVO} from '@/api/guide/sightscategory'
+/** 商品分类选择组件 */
+defineOptions({ name: 'SightsCategoryDisplay' })
+const { locale } = useI18n(); // 国际化
+const props = defineProps({
+  // 选中的ID
+  ids: {
+    type: String,
+    required: true
+  }
+})
+
+/** 初始化 **/
+const categoryList = ref<SightsCategoryVO[]>([]) // 分类树
+onMounted(async () => {
+  // 获得分类树
+  const lang = locale.value.split('-')[0]; // 获取语言代码
+  const data = await SightsCategoryApi.getSightCategorys(props.ids)
+  // const data = await SightsCategoryApi.getSightCategorys("85,86")
+  data.forEach(category => {
+    switch (lang) {
+      case 'zh':
+        category.name = category.nameZh;
+        break;
+      case 'ja':
+        category.name = category.nameJa;
+        break;
+      case 'en':
+      default:
+        category.name = category.nameEn;
+        break;
+    }
+});
+  categoryList.value = data//handleTree(data, 'id', 'parentId')
+})
+</script>

+ 68 - 0
src/views/guide/sights/components/SightsCategorySelect.vue

@@ -0,0 +1,68 @@
+<template>
+  <el-tree-select
+    v-model="selectCategoryId"
+    :data="categoryList"
+    :props="defaultProps"
+    :multiple="multiple"
+    :show-checkbox="multiple"
+    class="w-1/1"
+    node-key="id"
+    :placeholder="t('guide.sight.selectAttractionCategory')"
+  />
+</template>
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import {SightsCategoryApi,SightsCategoryVO} from '@/api/guide/sightscategory'
+import { oneOfType } from 'vue-types'
+import { propTypes } from '@/utils/propTypes'
+
+/** 商品分类选择组件 */
+defineOptions({ name: 'SightsCategorySelect' })
+const { t,locale } = useI18n(); // 国际化
+
+const props = defineProps({
+  // 选中的ID
+  modelValue: oneOfType<number | number[]>([Number, Array<Number>]),
+  // 是否多选
+  multiple: propTypes.bool.def(true),
+  // 上级品类的编号
+  parentId: propTypes.number.def(undefined)
+})
+
+/** 选中的分类 ID */
+const selectCategoryId = computed({
+  get: () => {
+    return props.modelValue
+  },
+  set: (val: number | number[]) => {
+    emit('update:modelValue', val)
+  }
+})
+
+/** 分类选择 */
+const emit = defineEmits(['update:modelValue'])
+
+/** 初始化 **/
+const categoryList = ref<SightsCategoryVO[]>([]) // 分类树
+onMounted(async () => {
+  // 获得分类树
+  const lang = locale.value.split('-')[0]; // 获取语言代码
+  const data = await SightsCategoryApi.getSightsCategoryList({ parentId: props.parentId })
+  data.forEach(category => {
+    switch (lang) {
+      case 'zh':
+        category.name = category.nameZh;
+        break;
+      case 'ja':
+        category.name = category.nameJa;
+        break;
+      case 'en':
+      default:
+        category.name = category.nameEn;
+        break;
+    }
+  // category.name = category.nameZh;
+});
+  categoryList.value = handleTree(data, 'id', 'parentId')
+})
+</script>

+ 160 - 0
src/views/guide/sights/components/SightsCommentForm.vue

@@ -0,0 +1,160 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1024px">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="130px"
+      v-loading="formLoading"
+    >
+      <el-form-item :label="t('guide.sight.ratingStars')" prop="scores">
+        <el-rate
+          v-model="formData.scores"
+          :texts="['oops', 'disappointed', 'normal', 'good', 'great']"
+          show-text
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.sceneryStars')" prop="sceneryScores">
+        <el-rate
+          v-model="formData.sceneryScores"
+          :texts="['oops', 'disappointed', 'normal', 'good', 'great']"
+          show-text
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.serviceStars')" prop="benefitScores">
+        <el-rate
+          v-model="formData.benefitScores"
+          :texts="['oops', 'disappointed', 'normal', 'good', 'great']"
+          show-text
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.reviewContent')" prop="content">
+        <QEditor v-model="formData.content" height="200px" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.tourDuration')" prop="durationTour">
+        <el-input-number v-model="formData.durationTour"/>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.likesCount')" prop="likeCount">
+        <el-input-number v-model="formData.likeCount" :min="1"/>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.image')" prop="picUrls">
+        <UploadImgs v-model="formData.picUrls" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">{{t('guide.common.confirm')}}</el-button>
+      <el-button @click="dialogVisible = false">{{t('guide.common.cancel')}}</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { SightsApi } from '@/api/guide/sights'
+
+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,
+  userId: undefined,
+  userNickname: undefined,
+  userAvatar: undefined,
+  anonymous: undefined,
+  sightsId: undefined,
+  sightsTitle: undefined,
+  visible: undefined,
+  scores: undefined,
+  sceneryScores: undefined,
+  benefitScores: undefined,
+  content: undefined,
+  durationTour: undefined,
+  likeCount: undefined,
+  picUrls: undefined,
+  replyStatus: undefined,
+  replyUserId: undefined,
+  replyContent: undefined,
+  replyTime: undefined
+})
+const formRules = reactive({
+  // anonymous: [{ required: true, message: '是否匿名不能为空', trigger: 'blur' }],
+  // sightsId: [{ required: true, message: t('guide.sight.multilingualType') + ' '+t('common.required'), trigger: 'blur' }],
+  scores: [{ required: true, message: t('guide.sight.ratingStars') + ' '+t('common.required'), trigger: 'blur' }],
+  sceneryScores: [{ required: true, message: t('guide.sight.sceneryStars') + ' '+t('common.required'), trigger: 'blur' }],
+  benefitScores: [{ required: true, message: t('guide.sight.serviceStars') + ' '+t('common.required'), trigger: 'blur' }],
+  content: [{ required: true, message: t('guide.sight.reviewContent') + ' '+t('common.required'),trigger: 'blur' }],
+  likeCount: [{ required: true, message: t('guide.sight.likesCount') + ' '+t('common.required'), trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, sightsId: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.sightsId = sightsId
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await SightsApi.getSightsComment(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await SightsApi.createSightsComment(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await SightsApi.updateSightsComment(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    userId: undefined,
+    userNickname: undefined,
+    userAvatar: undefined,
+    anonymous: undefined,
+    sightsId: undefined,
+    sightsTitle: undefined,
+    visible: undefined,
+    scores: undefined,
+    sceneryScores: undefined,
+    benefitScores: undefined,
+    content: undefined,
+    durationTour: undefined,
+    likeCount: undefined,
+    picUrls: undefined,
+    replyStatus: undefined,
+    replyUserId: undefined,
+    replyContent: undefined,
+    replyTime: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 91 - 0
src/views/guide/sights/components/SightsCommentForm2.vue

@@ -0,0 +1,91 @@
+<!-- 商品发布 - 其它设置 -->
+<template>
+  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+    <el-form-item label="商品排序" prop="sort">
+      <el-input-number
+        v-model="formData.sort"
+        :min="0"
+        placeholder="请输入商品排序"
+        class="w-80!"
+      />
+    </el-form-item>
+    <el-form-item label="赠送积分" prop="giveIntegral">
+      <el-input-number
+        v-model="formData.giveIntegral"
+        :min="0"
+        placeholder="请输入赠送积分"
+        class="w-80!"
+      />
+    </el-form-item>
+    <el-form-item label="虚拟销量" prop="virtualSalesCount">
+      <el-input-number
+        v-model="formData.virtualSalesCount"
+        :min="0"
+        placeholder="请输入虚拟销量"
+        class="w-80!"
+      />
+    </el-form-item>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import type { Spu } from '@/api/mall/product/spu'
+import { PropType } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+import { copyValueToTarget } from '@/utils'
+
+defineOptions({ name: 'ProductOtherForm' })
+
+const message = useMessage() // 消息弹窗
+
+const props = defineProps({
+  propFormData: {
+    type: Object as PropType<Spu>,
+    default: () => {}
+  },
+  isDetail: propTypes.bool.def(false) // 是否作为详情组件
+})
+
+const formRef = ref() // 表单Ref
+// 表单数据
+const formData = ref<Spu>({
+  sort: 0, // 商品排序
+  giveIntegral: 0, // 赠送积分
+  virtualSalesCount: 0 // 虚拟销量
+})
+// 表单规则
+const rules = reactive({
+  sort: [required],
+  giveIntegral: [required],
+  virtualSalesCount: [required]
+})
+
+/** 将传进来的值赋值给 formData */
+watch(
+  () => props.propFormData,
+  (data) => {
+    if (!data) {
+      return
+    }
+    copyValueToTarget(formData.value, data)
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 表单校验 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+  if (!formRef) return
+  try {
+    await unref(formRef)?.validate()
+    // 校验通过更新数据
+    Object.assign(props.propFormData, formData.value)
+  } catch (e) {
+    message.error('【其它设置】不完善,请填写相关信息')
+    emit('update:activeName', 'other')
+    throw e // 目的截断之后的校验
+  }
+}
+defineExpose({ validate })
+</script>

+ 153 - 0
src/views/guide/sights/components/SightsCommentList.vue

@@ -0,0 +1,153 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-button
+      type="primary"
+      plain
+      @click="openForm('create')"
+      v-hasPermi="['guide:sights:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> {{t('guide.common.add')}}
+    </el-button>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column :label="t('guide.sight.id')" align="center" prop="id" />
+      <el-table-column :label="t('guide.sight.ratingStars')" align="center" prop="scores" >
+        <template #default="scope">
+          <el-rate
+            v-model="scope.row.scores"
+            disabled
+            show-score
+            text-color="#ff9900"
+          />
+      </template>
+      </el-table-column>
+      <el-table-column :label="t('guide.sight.sceneryStars')" align="center" prop="sceneryScores"  >
+        <template #default="scope">
+          <el-rate
+            v-model="scope.row.sceneryScores"
+            disabled
+            show-score
+            text-color="#ff9900"
+          />
+      </template>
+      </el-table-column>
+      <el-table-column :label="t('guide.sight.serviceStars')" align="center" prop="benefitScores"  >
+        <template #default="scope">
+          <el-rate
+            v-model="scope.row.benefitScores"
+            disabled
+            show-score
+            text-color="#ff9900"
+          />
+      </template>
+      </el-table-column>
+      <el-table-column :label="t('guide.sight.reviewContent')" align="center" prop="content">
+        <template #default="scope">
+{{scope.row.content.replace(/<[^>]*>/g, '')}}
+
+      </template>
+      </el-table-column>
+      <el-table-column :label="t('guide.sight.likesCount')" align="center" prop="likeCount" />
+      <el-table-column :label="t('guide.common.actions')" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['guide:sights:update']"
+          >
+          {{t('guide.common.edit')}}
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['guide:sights:delete']"
+          >
+          {{t('guide.common.delete')}}
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+    <!-- 表单弹窗:添加/修改 -->
+    <SightsCommentForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { SightsApi } from '@/api/guide/sights'
+import SightsCommentForm from './SightsCommentForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  sightsId: undefined // 观光景点编号,关联 SightDO 的 id(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  sightsId: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.sightsId,
+  (val) => {
+    queryParams.sightsId = val
+    handleQuery()
+  },
+  { immediate: false }
+)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SightsApi.getSightsCommentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.sightsId) {
+    message.error('请选择一个观光景点')
+    return
+  }
+  formRef.value.open(type, id, props.sightsId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await SightsApi.deleteSightsComment(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+</script>

+ 191 - 0
src/views/guide/sights/components/SightsI18nExtensionForm.vue

@@ -0,0 +1,191 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1024px">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+       <el-form-item :label="t('guide.sight.multilingualType')" prop="languageType">
+        <el-radio-group v-model="formData.languageType">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.I8N_TYPE)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.title')" prop="title">
+        <el-input v-model="formData.title" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.subtitle')" prop="subtitle">
+        <el-input v-model="formData.subtitle"  />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.introduction')" prop="overview">
+        <el-input v-model="formData.overview" :rows="4" type="textarea" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.attractionTextualDescription')" prop="description">
+        <QEditor v-model="formData.description" height="350px" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.address')" prop="address">
+        <el-input v-model="formData.address" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.ticketsAndReservations')" prop="bookingDetails">
+        <el-input v-model="formData.bookingDetails" />
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.keywordGroup')" prop="keywords">
+        <el-input v-model="formData.keywords" />
+        <div class="flex gap-2">
+    <el-tag
+      v-for="tag in dynamicTags"
+      :key="tag"
+      closable
+      :disable-transitions="false"
+      @close="handleClose(tag)"
+    >
+      {{ tag }}
+    </el-tag>
+    <el-input
+      v-if="inputVisible"
+      ref="InputRef"
+      v-model="inputValue"
+      class="w-20"
+      size="small"
+      @keyup.enter="handleInputConfirm"
+      @blur="handleInputConfirm"
+    />
+    <el-button v-else class="button-new-tag" size="small" @click="showInput">
+      + {{t('guide.common.add')}}
+    </el-button>
+  </div>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.additionalInfo')" prop="memo">
+        <el-input :rows="3"  v-model="formData.memo" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">{{t('guide.common.confirm')}}</el-button>
+      <el-button @click="dialogVisible = false">{{t('guide.common.cancel')}}</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { SightsApi } from '@/api/guide/sights'
+import { ElInput } from 'element-plus'
+import '@vueup/vue-quill/dist/vue-quill.snow.css';
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+
+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,
+  sightsId: undefined,
+  languageType: undefined,
+  title: undefined,
+  subtitle: undefined,
+  overview: undefined,
+  description: undefined,
+  address: undefined,
+  bookingDetails: undefined,
+  keywords: undefined,
+  memo: undefined
+})
+const formRules = reactive({
+  // sightsId: [{ required: true, message: '景点编号 关联 sightDO 的 id 编号不能为空', trigger: 'blur' }],
+  languageType: [{ required: true, message: t('guide.sight.multilingualType') + ' '+t('common.required'), trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, sightsId: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.sightsId = sightsId
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await SightsApi.getSightsI18nExtension(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  formData.value.keywords = dynamicTags
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await SightsApi.createSightsI18nExtension(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await SightsApi.updateSightsI18nExtension(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    sightsId: undefined,
+    languageType: undefined,
+    title: undefined,
+    subtitle: undefined,
+    overview: undefined,
+    description: undefined,
+    address: undefined,
+    bookingDetails: undefined,
+    keywords: undefined,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
+const inputValue = ref('')
+const dynamicTags = ref(['文化', '风景', '历史'])
+const inputVisible = ref(false)
+const InputRef = ref<InstanceType<typeof ElInput>>()
+
+const handleClose = (tag: string) => {
+  dynamicTags.value.splice(dynamicTags.value.indexOf(tag), 1)
+}
+
+const showInput = () => {
+  inputVisible.value = true
+  nextTick(() => {
+    InputRef.value!.input!.focus()
+  })
+}
+
+const handleInputConfirm = () => {
+  if (inputValue.value) {
+    dynamicTags.value.push(inputValue.value)
+  }
+  inputVisible.value = false
+  inputValue.value = ''
+}
+</script>

+ 157 - 0
src/views/guide/sights/components/SightsI18nExtensionForm2.vue

@@ -0,0 +1,157 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    v-loading="formLoading"
+    label-width="0px"
+    :inline-message="true"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="序号" type="index" width="100" />
+       <el-table-column label="多语言类型(0:英语 1:汉语 2:日语。。。)" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.languageType`" :rules="formRules.languageType" class="mb-0px!">
+            <el-select v-model="row.languageType" placeholder="请选择多语言类型(0:英语 1:汉语 2:日语。。。)">
+                <el-option label="请选择字典生成" value="" />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="标题" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.title`" :rules="formRules.title" class="mb-0px!">
+            <el-input v-model="row.title" placeholder="请输入标题" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="副标题()" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.subtitle`" :rules="formRules.subtitle" class="mb-0px!">
+            <el-input v-model="row.subtitle" placeholder="请输入副标题()" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="简介()" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.overview`" :rules="formRules.overview" class="mb-0px!">
+            <el-input v-model="row.overview" placeholder="请输入简介()" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="景点图文说明()" min-width="400">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.description`" :rules="formRules.description" class="mb-0px!">
+            <Editor v-model="row.description" height="150px" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="地址()" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.address`" :rules="formRules.address" class="mb-0px!">
+            <el-input v-model="row.address" placeholder="请输入地址()" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="门票&预约信息()" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.bookingDetails`" :rules="formRules.bookingDetails" class="mb-0px!">
+            <el-input v-model="row.bookingDetails" placeholder="请输入门票&预约信息()" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="关键字数组,以逗号分隔()" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.keywords`" :rules="formRules.keywords" class="mb-0px!">
+            <el-input v-model="row.keywords" placeholder="请输入关键字数组,以逗号分隔()" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="补充说明()" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
+            <el-input v-model="row.memo" placeholder="请输入补充说明()" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
+  <el-row justify="center" class="mt-3">
+    <el-button @click="handleAdd" round>+ 添加观光景点对语言扩充信息</el-button>
+  </el-row>
+</template>
+<script setup lang="ts">
+import { SightsApi } from '@/api/guide/sights'
+
+const props = defineProps<{
+  sightsId: undefined // 景点编号 关联 sightDO 的 id 编号(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  sightsId: [{ required: true, message: '景点编号 关联 sightDO 的 id 编号不能为空', trigger: 'blur' }],
+  languageType: [{ required: true, message: '多语言类型(0:英语 1:汉语 2:日语。。。)不能为空', trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.sightsId,
+  async (val) => {
+    // 1. 重置表单
+    formData.value = []
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+      formData.value = await SightsApi.getSightsI18nExtensionListBySightsId(val)
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+    id: undefined,
+    sightsId: undefined,
+    languageType: undefined,
+    title: undefined,
+    subtitle: undefined,
+    overview: undefined,
+    description: undefined,
+    address: undefined,
+    bookingDetails: undefined,
+    keywords: undefined,
+    memo: undefined
+  }
+  row.sightsId = props.sightsId
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index) => {
+  formData.value.splice(index, 1)
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>

+ 137 - 0
src/views/guide/sights/components/SightsI18nExtensionList.vue

@@ -0,0 +1,137 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-button
+      type="primary"
+      plain
+      @click="openForm('create')"
+      v-hasPermi="['guide:sights:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> {{t('guide.common.add')}}
+    </el-button>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column :label="t('guide.sight.id')" align="center" prop="id" />
+       <el-table-column :label="t('guide.sight.multilingualType')" align="center" prop="languageType">
+       <template #default="scope">
+          <dict-tag :type="DICT_TYPE.I8N_TYPE" :value="scope.row.languageType" />
+        </template>
+        </el-table-column>
+      <el-table-column :label="t('guide.sight.title')" align="center" prop="title" />
+      <el-table-column :label="t('guide.sight.introduction')" align="center" prop="overview" />
+      <el-table-column :label="t('guide.sight.ticketsAndReservations')" align="center" prop="bookingDetails" />
+      <el-table-column :label="t('guide.sight.keywordGroup')" align="center" prop="keywords" >
+        <template #default="scope">
+          <el-tag>{{ scope.row.keywords }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        :label="t('guide.category.creationTime')"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column :label="t('guide.common.actions')" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['guide:sights:update']"
+          >
+          {{t('guide.common.edit')}}
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['guide:sights:delete']"
+          >
+          {{t('guide.common.delete')}}
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+    <!-- 表单弹窗:添加/修改 -->
+    <SightsI18nExtensionForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { SightsApi } from '@/api/guide/sights'
+import SightsI18nExtensionForm from './SightsI18nExtensionForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  sightsId: undefined // 景点编号 关联 sightDO 的 id 编号(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  sightsId: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.sightsId,
+  (val) => {
+    queryParams.sightsId = val
+    handleQuery()
+  },
+  { immediate: false }
+)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SightsApi.getSightsI18nExtensionPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.sightsId) {
+    message.error('请选择一个观光景点')
+    return
+  }
+  formRef.value.open(type, id, props.sightsId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await SightsApi.deleteSightsI18nExtension(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+</script>

+ 242 - 0
src/views/guide/sights/index.vue

@@ -0,0 +1,242 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item :label= "t('guide.sight.attractionCategory')" prop="categoryIds" label-width="70px">
+        <SightsCategorySelect v-model="queryParams.categoryIds" class="!w-240px"/>
+      </el-form-item>
+      <el-form-item :label="t('guide.sight.region')" prop="geographicalIds" label-width="70px">
+        <el-cascader v-model="queryParams.geographicalIds" :options="areaList" :props="defaultProps" />
+      </el-form-item>
+
+      <el-form-item :label="t('guide.category.creationTime')" prop="createTime" label-width="150px">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          :start-placeholder="t('guide.category.startDate')"
+          :end-placeholder="t('guide.category.endDate')"
+          :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" /> {{t('guide.common.search')}}</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{t('guide.common.reset')}}</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['guide:sights:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> {{t('guide.common.add')}}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['guide:sights:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> {{t('guide.common.export')}}
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      highlight-current-row
+      @current-change="handleCurrentChange"
+    >
+      <el-table-column :label="t('guide.sight.id')" align="center" prop="id" />
+      <el-table-column :label="t('guide.sight.attractionCategory')" align="center" prop="categoryIds"  min-width="120">
+        <template #default="{ row }">
+          <div class="flex">
+            <SightsCategoryDisplay :ids="row.categoryIds"/>          
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('guide.sight.attractionInfo')" min-width="100">
+        <template #default="{ row }">
+          <div class="flex">
+            <el-image
+              fit="cover"
+              :src="row.picUrl"
+              class="flex-none w-50px h-50px"
+              @click="imagePreview(row.picUrl)"
+            />
+            <div class="ml-4 overflow-hidden">
+              <el-tooltip effect="dark" :content="row.name" placement="top">
+                <div>
+                  {{ row.url }}
+                </div>
+              </el-tooltip>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('guide.sight.phoneNumber')" align="center" prop="tel" />
+      <el-table-column :label="t('guide.sight.openingHours')" align="center" prop="opentime" />
+      <el-table-column :label="t('guide.category.enableStatus')" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('guide.common.actions')" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['guide:sights:update']"
+          >
+            {{t('guide.common.edit')}}
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['guide:sights:delete']"
+          >
+            {{t('guide.common.delete')}}
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <SightsForm ref="formRef" @success="getList" />
+  <!-- 子表的列表 -->
+  <ContentWrap>
+    <el-tabs model-value="sightsI18nExtension">
+      <el-tab-pane :label="t('guide.sight.multilingualExpansionInfo')" name="sightsI18nExtension">
+        <SightsI18nExtensionList :sights-id="currentRow.id" />
+      </el-tab-pane>
+      <el-tab-pane :label="t('guide.sight.comments')" name="sightsComment">
+        <SightsCommentList :sights-id="currentRow.id" />
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import download from '@/utils/download'
+import { SightsApi, SightsVO } from '@/api/guide/sights'
+import SightsForm from './SightsForm.vue'
+import SightsCommentList from './components/SightsCommentList.vue'
+import SightsI18nExtensionList from './components/SightsI18nExtensionList.vue'
+import { createImageViewer } from '@/components/ImageViewer'
+import SightsCategorySelect from './components/SightsCategorySelect.vue'
+import SightsCategoryDisplay from './components/SightsCategoryDisplay.vue'
+/** 观光景点 列表 */
+defineOptions({ name: 'Sights' })
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<SightsVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  categoryIds: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SightsApi.getSightsPage(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 handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await SightsApi.deleteSights(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+/** 商品图预览 */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    urlList: [imgUrl]
+  })
+}
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await SightsApi.exportSights(queryParams)
+    download.excel(data, '观光景点.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 选中行操作 */
+const currentRow = ref({}) // 选中行
+const handleCurrentChange = (row) => {
+  currentRow.value = row
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  getList()
+})
+</script>

+ 158 - 0
src/views/guide/sightscategory/SightsCategoryForm.vue

@@ -0,0 +1,158 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="160px"
+      v-loading="formLoading"
+    >
+      <el-form-item :label="t('guide.category.parentCategoryID')" prop="parentId">
+        <el-tree-select
+          v-model="formData.parentId"
+          :data="sightsCategoryTree"
+          :props="{...defaultProps, label: 'nameZh'}"
+          check-strictly
+          default-expand-all
+          :placeholder="t('guide.category.selectParentCategoryID')"
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.category.chineseName')" prop="nameZh">
+        <el-input v-model="formData.nameZh" :placeholder="t('guide.category.enterChineseName')" />
+      </el-form-item>
+      <el-form-item :label="t('guide.category.japaneseName')" prop="nameJa">
+        <el-input v-model="formData.nameJa" :placeholder="t('guide.category.enterJapaneseName')" />
+      </el-form-item>
+      <el-form-item :label="t('guide.category.englishName')" prop="nameEn">
+        <el-input v-model="formData.nameEn" :placeholder="t('guide.category.enterEnglishName')" />
+      </el-form-item>
+      <!-- <el-form-item label="分类XX名称" prop="nameOther">
+        <el-input v-model="formData.nameOther" placeholder="请输入分类XX名称" />
+      </el-form-item> -->
+      <el-form-item :label="t('guide.category.mobileCategoryImage')" prop="picUrl">
+        <UploadImg v-model="formData.picUrl" />
+      </el-form-item>
+      <el-form-item :label="t('guide.category.categorySort')" prop="sort">
+        <el-input-number v-model="formData.sort" :step="1" :placeholder="t('guide.category.enterCategorySort')" />
+      </el-form-item>
+      <el-form-item :label="t('guide.category.enableStatus')" 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>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">{{t('guide.common.confirm')}}</el-button>
+      <el-button @click="dialogVisible = false">{{t('guide.common.cancel')}}</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { SightsCategoryApi, SightsCategoryVO } from '@/api/guide/sightscategory'
+import { defaultProps, handleTree } from '@/utils/tree'
+
+/** 观光景点分类 表单 */
+defineOptions({ name: 'SightsCategoryForm' })
+
+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,
+  parentId: undefined,
+  nameZh: undefined,
+  nameJa: undefined,
+  nameEn: undefined,
+  nameOther: undefined,
+  picUrl: undefined,
+  sort: undefined,
+  status: undefined
+})
+const formRules = reactive({
+  parentId: [{ required: true, message: t('guide.category.selectParentCategoryID'), trigger: 'blur' }],
+  nameZh: [{ required: true, message: t('guide.category.enterChineseName'), trigger: 'blur' }],
+  picUrl: [{ required: true, message: t('guide.category.mobileCategoryImage') + ' '+t('common.required'), trigger: 'blur' }],
+  status: [{ required: true, message: t('guide.category.enableStatus') + ' '+t('common.required') , trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const sightsCategoryTree = 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 SightsCategoryApi.getSightsCategory(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+  await getSightsCategoryTree()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as SightsCategoryVO
+    if (formType.value === 'create') {
+      await SightsCategoryApi.createSightsCategory(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await SightsCategoryApi.updateSightsCategory(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    parentId: undefined,
+    nameZh: undefined,
+    nameJa: undefined,
+    nameEn: undefined,
+    nameOther: undefined,
+    picUrl: undefined,
+    sort: undefined,
+    status: undefined
+  }
+  formRef.value?.resetFields()
+}
+
+/** 获得观光景点分类树 */
+const getSightsCategoryTree = async () => {
+  sightsCategoryTree.value = []
+  const data = await SightsCategoryApi.getSightsCategoryList()
+  const root: Tree = { id: 0, name: t('guide.category.topTouristAttractionCategory') , children: [] }
+  root.children = handleTree(data, 'id', 'parentId')
+  sightsCategoryTree.value.push(root)
+}
+</script>

+ 254 - 0
src/views/guide/sightscategory/index.vue

@@ -0,0 +1,254 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item :label="t('guide.category.chineseName')" prop="nameZh">
+        <el-input
+          v-model="queryParams.nameZh"
+          :placeholder="t('guide.category.enterChineseName')"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.category.japaneseName')" prop="nameJa">
+        <el-input
+          v-model="queryParams.nameJa"
+          :placeholder="t('guide.category.enterJapaneseName')"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.category.englishName')" prop="nameEn">
+        <el-input
+          v-model="queryParams.nameEn"
+          :placeholder="t('guide.category.enterEnglishName')"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item :label="t('guide.category.creationTime')" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          :start-placeholder="t('guide.category.startDate')"
+          :end-placeholder="t('guide.category.endDate')"
+          :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" /> {{t('guide.common.search')}}</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{t('guide.common.reset')}}</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['guide:sights-category:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> {{t('guide.common.add')}}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['guide:sights-category:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> {{t('guide.common.export')}}
+        </el-button>
+        <el-button type="danger" plain @click="toggleExpandAll">
+          <Icon icon="ep:sort" class="mr-5px" /> {{t('guide.common.actions.expandexpandCollapse')}}
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+      v-if="refreshTable"
+    >
+      <el-table-column :label="t('guide.category.categoryID')" align="center" prop="id" />
+      <el-table-column :label="t('guide.category.parentCategoryID')" align="center" prop="parentId" />
+      <el-table-column :label="t('guide.category.chineseName')" align="center" prop="nameZh" />
+      <el-table-column :label="t('guide.category.japaneseName')" align="center" prop="nameJa" />
+      <el-table-column :label="t('guide.category.englishName')" align="center" prop="nameEn" />
+      <!-- <el-table-column label="分类XX名称" align="center" prop="nameOther" /> -->
+      <el-table-column :label="t('guide.category.mobileCategoryImage')" align="center" prop="picUrl" >
+      <template #default="{ row }">
+          <div class="flex">
+            <el-image
+              fit="cover"
+              :src="row.picUrl"
+              class="flex-none w-50px h-50px"
+              @click="imagePreview(row.picUrl)"
+            />
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('guide.category.categorySort')" align="center" prop="sort" />
+      <el-table-column :label="t('guide.category.enableStatus')" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        :label="t('guide.category.creationTime')"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column :label="t('guide.common.actions')" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['guide:sights-category:update']"
+          >
+            {{t('guide.common.edit')}}
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['guide:sights-category:delete']"
+          >
+            {{t('guide.common.delete')}}
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <SightsCategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { handleTree } from '@/utils/tree'
+import download from '@/utils/download'
+import { SightsCategoryApi, SightsCategoryVO } from '@/api/guide/sightscategory'
+import SightsCategoryForm from './SightsCategoryForm.vue'
+import { createImageViewer } from '@/components/ImageViewer'
+
+/** 观光景点分类 列表 */
+defineOptions({ name: 'SightsCategory' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<SightsCategoryVO[]>([]) // 列表的数据
+const queryParams = reactive({
+  nameZh: undefined,
+  nameJa: undefined,
+  nameEn: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+/** 商品图预览 */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    urlList: [imgUrl]
+  })
+}
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SightsCategoryApi.getSightsCategoryList(queryParams)
+    list.value = handleTree(data, 'id', 'parentId')
+  } 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 handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await SightsCategoryApi.deleteSightsCategory(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await SightsCategoryApi.exportSightsCategory(queryParams)
+    download.excel(data, '观光景点分类.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 展开/折叠操作 */
+const isExpandAll = ref(true) // 是否展开,默认全部展开
+const refreshTable = ref(true) // 重新渲染表格状态
+const toggleExpandAll = async () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  await nextTick()
+  refreshTable.value = true
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 101 - 0
src/views/guide/statistics/guide/components/GuideRank.vue

@@ -0,0 +1,101 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <!-- 标题 -->
+      <div class="flex flex-row items-center justify-between">
+        <CardTitle :title="t('statistics.guide_rank')" />
+        <!-- 查询条件 -->
+        <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="handleDateRangeChange" />
+      </div>
+    </template>
+    <!-- 排行列表 -->
+    <el-table v-loading="loading" :data="list" @sort-change="handleSortChange">
+      <el-table-column :label="t('statistics.guide_id')" prop="guideId" min-width="70" />
+      <el-table-column :label="t('statistics.guide_image')" align="center" prop="picUrl" width="80">
+        <template #default="{ row }">
+          <el-image
+            :src="row.picUrl"
+            :preview-src-list="[row.picUrl]"
+            class="h-30px w-30px"
+            preview-teleported
+          />
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('statistics.guide_nickname')" prop="nickName" min-width="100" :show-overflow-tooltip="true" />
+      <el-table-column :label="t('statistics.views')" prop="browseCount" min-width="90" sortable="custom" />
+      <el-table-column :label="t('statistics.visitors')" prop="browseUserCount" min-width="100" sortable="custom" />
+      <el-table-column :label="t('statistics.added_to_cart')" prop="cartCount" min-width="205" sortable="custom" />
+      <el-table-column :label="t('statistics.ordered_items')" prop="orderCount" min-width="105" sortable="custom" />
+      <el-table-column :label="t('statistics.paid_items')" prop="orderPayCount" min-width="145" sortable="custom" />
+      <el-table-column :label="t('statistics.payment_amount')" prop="orderPayPrice" min-width="105" sortable="custom" />
+      <el-table-column :label="t('statistics.favorites')" prop="favoriteCount" min-width="120" sortable="custom" />
+      <el-table-column
+        :label="t('statistics.conversion_rate')"
+        prop="browseConvertPercent"
+        min-width="190"
+        sortable="custom"
+        :formatter="formatConvertRate"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getGuideList"
+    />
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { GuideStatisticsApi, GuideStatisticsVO } from '@/api/guide/statistics/guide'
+import { CardTitle } from '@/components/Card'
+import { buildSortingField } from '@/utils'
+
+/** 商品排行 */
+defineOptions({ name: 'GuideRank' })
+const { t } = useI18n()
+// 格式化:访客-支付转化率
+const formatConvertRate = (row: GuideStatisticsVO) => {
+  return `${row.browseConvertPercent}%`
+}
+
+const handleSortChange = (params: any) => {
+  queryParams.sortingFields = [buildSortingField(params)]
+  getGuideList()
+}
+
+const handleDateRangeChange = (times: any[]) => {
+  queryParams.times = times as []
+  getGuideList()
+}
+
+const shortcutDateRangePicker = ref()
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  times: [],
+  sortingFields: {}
+})
+const loading = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref<GuideStatisticsVO[]>([]) // 列表的数据
+
+/** 查询商品列表 */
+const getGuideList = async () => {
+  loading.value = true
+  try {
+    const data = await GuideStatisticsApi.getGuideStatisticsRankPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getGuideList()
+})
+</script>
+<style lang="scss" scoped></style>

+ 294 - 0
src/views/guide/statistics/guide/components/GuideSummary.vue

@@ -0,0 +1,294 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <!-- 标题 -->
+      <div class="flex flex-row items-center justify-between">
+        <CardTitle :title="t('statistics.guide_overview')" />
+        <!-- 查询条件 -->
+        <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getGuideTrendData">
+          <el-button
+            class="ml-4"
+            @click="handleExport"
+            :loading="exportLoading"
+            v-hasPermi="['statistics:product:export']"
+          >
+            <Icon icon="ep:download" class="mr-1" />{{ t('action.export') }}
+          </el-button>
+        </ShortcutDateRangePicker>
+      </div>
+    </template>
+    <!-- 统计值 -->
+    <el-row :gutter="16">
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.views')"
+          :tooltip="t('statistics.guide_page_views')"
+          icon="ep:view"
+          icon-color="bg-blue-100"
+          icon-bg-color="text-blue-500"
+          :value="(trendSummary?.value?.browseCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.browseCount,
+              trendSummary?.reference?.browseCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.visitors')"
+          :tooltip="t('statistics.guide_visitors')"
+          icon="ep:user-filled"
+          icon-color="bg-purple-100"
+          icon-bg-color="text-purple-500"
+          :value="(trendSummary?.value?.browseUserCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.browseUserCount,
+              trendSummary?.reference?.browseUserCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.paid_items')"
+          :tooltip="t('statistics.paid_items_count')"
+          icon="fa-solid:money-check-alt"
+          icon-color="bg-yellow-100"
+          icon-bg-color="text-yellow-500"
+          :value="(trendSummary?.value?.orderPayCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.orderPayCount,
+              trendSummary?.reference?.orderPayCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.payment_amount')"
+          :tooltip="t('statistics.payment_total')"
+          icon="ep:warning-filled"
+          icon-color="bg-green-100"
+          icon-bg-color="text-green-500"
+          prefix="¥"
+          :value="(trendSummary?.value?.orderPayPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.orderPayPrice,
+              trendSummary?.reference?.orderPayPrice
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.refund_items')"
+          :tooltip="t('statistics.refund_count')"
+          icon="fa-solid:wallet"
+          icon-color="bg-cyan-100"
+          icon-bg-color="text-cyan-500"
+          :value="(trendSummary?.value?.afterSaleCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.afterSaleCount,
+              trendSummary?.reference?.afterSaleCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.refund_amounts')"
+          :tooltip="t('statistics.refund_amount')"
+          icon="fa-solid:award"
+          icon-color="bg-yellow-100"
+          icon-bg-color="text-yellow-500"
+          prefix="¥"
+          :value="(trendSummary?.value?.afterSaleRefundPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.afterSaleRefundPrice,
+              trendSummary?.reference?.afterSaleRefundPrice
+            )
+          "
+        />
+      </el-col>
+    </el-row>
+    <!-- 折线图 -->
+    <el-skeleton :loading="trendLoading" animated>
+      <Echart :height="500" :options="lineChartOptions" />
+    </el-skeleton>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { GuideStatisticsApi, GuideStatisticsVO } from '@/api/guide/statistics/guide'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { EChartsOption } from 'echarts'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+import { calculateRelativeRate } from '@/utils'
+import download from '@/utils/download'
+import { CardTitle } from '@/components/Card'
+import * as DateUtil from '@/utils/formatTime'
+import dayjs from 'dayjs'
+
+/** 商品概况 */
+defineOptions({ name: 'GuideSummary' })
+const { t } = useI18n()
+const message = useMessage() // 消息弹窗
+
+const trendLoading = ref(true) // 商品状态加载中
+const exportLoading = ref(false) // 导出的加载中
+const trendSummary = ref<GuideDataComparisonRespVO<GuideStatisticsVO>>() // 商品状况统计数据
+const shortcutDateRangePicker = ref()
+
+/** 折线图配置 */
+const lineChartOptions = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['time', 'browseCount', 'browseUserCount', 'orderPayPrice', 'afterSaleRefundPrice'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+    { name: t('statistics.views'), type: 'line', smooth: true, itemStyle: { color: '#B37FEB' } },
+    { name: t('statistics.visitors'), type: 'line', smooth: true, itemStyle: { color: '#FFAB2B' } },
+    { name: t('statistics.payment_amount'), type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#1890FF' } },
+    { name: t('statistics.refund_amounts'), type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#00C050' } }
+  ],
+  toolbox: {
+    feature: {
+      // 数据区域缩放
+      dataZoom: {
+        yAxisIndex: false // Y轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '商品状况' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: true,
+    axisTick: {
+      show: false
+    }
+  },
+  yAxis: [
+    {
+      type: 'value',
+      name: t('statistics.quantity'),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        textStyle: {
+          color: '#7F8B9C'
+        }
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: '#F5F7F9'
+        }
+      }
+    },
+    {
+      type: 'value',
+      name: t('statistics.amount'),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        textStyle: {
+          color: '#7F8B9C'
+        }
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: '#F5F7F9'
+        }
+      }
+    }
+  ]
+}) as EChartsOption
+
+/** 处理商品状况查询 */
+const getGuideTrendData = async () => {
+  trendLoading.value = true
+  // 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
+  const times = shortcutDateRangePicker.value.times
+  if (DateUtil.isSameDay(times[0], times[1])) {
+    // 前天
+    times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
+  }
+  // 查询数据
+  await Promise.all([getGuideTrendSummary(), getGuideStatisticsList()])
+  trendLoading.value = false
+}
+
+/** 查询商品状况数据统计 */
+const getGuideTrendSummary = async () => {
+  const times = shortcutDateRangePicker.value.times
+  trendSummary.value = await GuideStatisticsApi.getGuideStatisticsAnalyse({ times })
+}
+
+/** 查询商品状况数据列表 */
+const getGuideStatisticsList = async () => {
+  // 查询数据
+  const times = shortcutDateRangePicker.value.times
+  const list: GuideStatisticsVO[] = await GuideStatisticsApi.getGuideStatisticsList({ times })
+  // 处理数据
+  for (let item of list) {
+    item.orderPayPrice = (item.orderPayPrice)
+    item.afterSaleRefundPrice = (item.afterSaleRefundPrice)
+  }
+  // 更新 Echarts 数据
+  if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+    lineChartOptions.dataset['source'] = list
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const times = shortcutDateRangePicker.value.times
+    const data = await GuideStatisticsApi.exportGuideStatisticsExcel({ times })
+    download.excel(data, t('statistics.guide_status_excel'))
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+</script>
+<style lang="scss" scoped></style>

+ 16 - 0
src/views/guide/statistics/guide/index.vue

@@ -0,0 +1,16 @@
+<template>
+  <!-- <doc-alert title="【统计】会员、商品、交易统计" url="https://doc.iocoder.cn/mall/statistics/" /> -->
+
+  <!-- 商品概览 -->
+  <GuideSummary />
+  <!-- 商品排行 -->
+  <GuideRank class="mt-16px" />
+</template>
+<script lang="ts" setup>
+import GuideSummary from './components/GuideSummary.vue'
+import GuideRank from './components/GuideRank.vue'
+
+/** 商品统计 */
+defineOptions({ name: 'GuideStatistics' })
+</script>
+<style lang="scss" scoped></style>

+ 121 - 0
src/views/guide/statistics/member/components/MemberFunnelCard.vue

@@ -0,0 +1,121 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="my--1.5 flex flex-row items-center justify-between">
+        <CardTitle :title="t('home.memberOverview')" />
+        <!-- 查询条件 -->
+        <ShortcutDateRangePicker @change="handleTimeRangeChange" />
+      </div>
+    </template>
+    <div class="min-w-225 py-1.75" v-loading="loading">
+      <div class="relative h-24 flex">
+        <div class="h-full w-75% bg-blue-50 <lg:w-35% <xl:w-55%">
+          <div class="ml-15 h-full flex flex-col justify-center">
+            <div class="font-bold">
+              {{t('home.registeredUsers')}}{{ analyseData?.comparison?.value?.registerUserCount || 0 }}
+            </div>
+            <div class="mt-2 text-3.5">
+              {{t('home.sequentialGrowthRate')}}{{
+                calculateRelativeRate(
+                  analyseData?.comparison?.value?.registerUserCount,
+                  analyseData?.comparison?.reference?.registerUserCount
+                )
+              }}%
+            </div>
+          </div>
+        </div>
+        <div
+          class="trapezoid1 ml--38.5 mt-1.5 h-full w-77 flex flex-col items-center justify-center bg-blue-5 text-3.5 text-white"
+        >
+          <span class="text-6 font-bold">{{ analyseData?.visitUserCount || 0 }}</span>
+          <span>{{t('home.visitors')}}</span>
+        </div>
+      </div>
+      <div class="relative h-24 flex">
+        <div class="h-full w-75% flex bg-cyan-50 <lg:w-35% <xl:w-55%">
+          <div class="ml-15 h-full flex flex-col justify-center">
+            <div class="font-bold">
+              {{t('home.activeUsers')}}{{ analyseData?.comparison?.value?.visitUserCount || 0 }}
+            </div>
+            <div class="mt-2 text-3.5">
+              {{t('home.sequentialGrowthRate')}}{{
+                calculateRelativeRate(
+                  analyseData?.comparison?.value?.visitUserCount,
+                  analyseData?.comparison?.reference?.visitUserCount
+                )
+              }}%
+            </div>
+          </div>
+        </div>
+        <div
+          class="trapezoid2 ml--28 mt-1.7 h-25 w-56 flex flex-col items-center justify-center bg-cyan-5 text-3.5 text-white"
+        >
+          <span class="text-6 font-bold">{{ analyseData?.orderUserCount || 0 }}</span>
+          <span>{{t('home.placeOrder')}}</span>
+        </div>
+      </div>
+      <div class="relative h-24 flex">
+        <div class="w-75% flex bg-slate-50 <lg:w-35% <xl:w-55%">
+          <div class="ml-15 h-full flex flex-row gap-x-16">
+            <div class="flex flex-col justify-center">
+              <div class="font-bold">
+                {{t('home.rechargedUsers')}}{{ analyseData?.comparison?.value?.rechargeUserCount || 0 }}
+              </div>
+              <div class="mt-2 text-3.5">
+                {{t('home.sequentialGrowthRate')}}{{
+                  calculateRelativeRate(
+                    analyseData?.comparison?.value?.rechargeUserCount,
+                    analyseData?.comparison?.reference?.rechargeUserCount
+                  )
+                }}%
+              </div>
+            </div>
+            <div class="flex flex-col justify-center">
+              <div class="font-bold">{{t('home.averageOrderValue')}}{{ (analyseData?.atv || 0) }}</div>
+            </div>
+          </div>
+        </div>
+        <div
+          class="trapezoid3 ml--18 mt-3.25 h-23 w-36 flex flex-col items-center justify-center bg-slate-5 text-3.5 text-white"
+        >
+          <span class="text-6 font-bold">{{ analyseData?.payUserCount || 0 }}</span>
+          <span>{{t('home.completedCustomers')}}</span>
+        </div>
+      </div>
+    </div>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import * as GuideMemberStatisticsApi from '@/api/guide/statistics/member'
+import dayjs from 'dayjs'
+import { calculateRelativeRate } from '@/utils'
+import { GuideMemberAnalyseRespVO } from '@/api/guide/statistics/member'
+import { CardTitle } from '@/components/Card'
+
+/** 会员概览卡片 */
+defineOptions({ name: 'MemberFunnelCard' })
+const { t } = useI18n()
+const loading = ref(true) // 加载中
+const analyseData = ref<GuideMemberAnalyseRespVO>() // 会员分析数据
+
+/** 查询会员概览数据列表 */
+const handleTimeRangeChange = async (times: [dayjs.ConfigType, dayjs.ConfigType]) => {
+  loading.value = true
+  // 查询数据
+  analyseData.value = await GuideMemberStatisticsApi.getMemberAnalyse({ times })
+  loading.value = false
+}
+</script>
+<style lang="scss" scoped>
+.trapezoid1 {
+  transform: perspective(5em) rotateX(-11deg);
+}
+
+.trapezoid2 {
+  transform: perspective(7em) rotateX(-20deg);
+}
+
+.trapezoid3 {
+  transform: perspective(3em) rotateX(-13deg);
+}
+</style>

+ 69 - 0
src/views/guide/statistics/member/components/MemberTerminalCard.vue

@@ -0,0 +1,69 @@
+<template>
+  <el-card shadow="never" v-loading="loading">
+    <template #header>
+      <CardTitle :title="t('statistics.member_terminal')" />
+    </template>
+    <Echart :height="300" :options="terminalChartOptions" />
+  </el-card>
+</template>
+<script lang="ts" setup>
+import * as MemberStatisticsApi from '@/api/mall/statistics/member'
+import { EChartsOption } from 'echarts'
+import { MemberTerminalStatisticsRespVO } from '@/api/mall/statistics/member'
+import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
+import { CardTitle } from '@/components/Card'
+
+/** 会员终端卡片 */
+defineOptions({ name: 'MemberTerminalCard' })
+const { t } = useI18n()
+const loading = ref(true) // 加载中
+
+/** 会员终端统计图配置 */
+const terminalChartOptions = reactive<EChartsOption>({
+  tooltip: {
+    trigger: 'item',
+    confine: true,
+    formatter: '{a} <br/>{b} : {c} ({d}%)'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'right'
+  },
+  roseType: 'area',
+  series: [
+    {
+      name: '会员终端',
+      type: 'pie',
+      label: {
+        show: false
+      },
+      labelLine: {
+        show: false
+      },
+      data: []
+    }
+  ]
+}) as EChartsOption
+
+/** 按照终端,查询会员统计列表 */
+const getMemberTerminalStatisticsList = async () => {
+  loading.value = true
+  const list = await MemberStatisticsApi.getMemberTerminalStatisticsList()
+  const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
+  terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
+    const userCount = list.find(
+      (item: MemberTerminalStatisticsRespVO) => item.terminal === dictData.value
+    )?.userCount
+    return {
+      name: dictData.label,
+      value: userCount || 0
+    }
+  })
+  loading.value = false
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getMemberTerminalStatisticsList()
+})
+</script>

+ 312 - 0
src/views/guide/statistics/member/index.vue

@@ -0,0 +1,312 @@
+<template>
+  <!-- <doc-alert title="【统计】会员、商品、交易统计" url="https://doc.iocoder.cn/mall/statistics/" /> -->
+
+  <div class="flex flex-col">
+    <el-row :gutter="16" class="summary">
+      <el-col v-loading="loading" :sm="12" :xs="24">
+        <SummaryCard
+          :value="summary?.userCount || 0"
+          icon="fa-solid:users"
+          icon-bg-color="text-blue-500"
+          icon-color="bg-blue-100"
+          :title="t('statistics.total_members')"
+        />
+      </el-col>
+      <!-- <el-col v-loading="loading" :sm="6" :xs="12">
+        <SummaryCard
+          :value="summary?.rechargeUserCount || 0"
+          icon="fa-solid:user"
+          icon-bg-color="text-purple-500"
+          icon-color="bg-purple-100"
+          title="累计充值人数"
+        />
+      </el-col>
+      <el-col v-loading="loading" :sm="6" :xs="12">
+        <SummaryCard
+          :decimals="2"
+          :value="fenToYuan(summary?.rechargePrice || 0)"
+          icon="fa-solid:money-check-alt"
+          icon-bg-color="text-yellow-500"
+          icon-color="bg-yellow-100"
+          prefix="¥"
+          title="累计充值金额"
+        />
+      </el-col> -->
+      <el-col v-loading="loading" :sm="12" :xs="24">
+        <SummaryCard
+          :value="(summary?.expensePrice || 0)"
+          icon="fa-solid:yen-sign"
+          icon-bg-color="text-green-500"
+          icon-color="bg-green-100"
+          prefix="¥"
+          :title="t('statistics.total_spent')"
+        />
+      </el-col>
+    </el-row>
+    <el-row :gutter="16" class="mb-4">
+      <el-col :md="24" :sm="24">
+        <!-- 会员概览 -->
+        <MemberFunnelCard />
+      </el-col>
+      <!-- <el-col :md="6" :sm="24"> -->
+        <!-- 会员终端 -->
+        <!-- <MemberTerminalCard /> -->
+      <!-- </el-col> -->
+    </el-row>
+    <!-- <el-row :gutter="16">
+      <el-col :md="18" :sm="24">
+        <el-card shadow="never">
+          <template #header>
+            <CardTitle title="会员地域分布" />
+          </template>
+          <el-row v-loading="loading">
+            <el-col :span="10">
+              <Echart :height="300" :options="areaChartOptions" />
+            </el-col>
+            <el-col :span="14">
+              <el-table :data="areaStatisticsList" :height="300">
+                <el-table-column
+                  :sort-method="(obj1, obj2) => obj1.areaName.localeCompare(obj2.areaName, 'zh-CN')"
+                  align="center"
+                  label="省份"
+                  min-width="80"
+                  prop="areaName"
+                  show-overflow-tooltip
+                  sortable
+                />
+                <el-table-column
+                  align="center"
+                  label="会员数量"
+                  min-width="105"
+                  prop="userCount"
+                  sortable
+                />
+                <el-table-column
+                  align="center"
+                  label="订单创建数量"
+                  min-width="135"
+                  prop="orderCreateUserCount"
+                  sortable
+                />
+                <el-table-column
+                  align="center"
+                  label="订单支付数量"
+                  min-width="135"
+                  prop="orderPayUserCount"
+                  sortable
+                />
+                <el-table-column
+                  :formatter="fenToYuanFormat"
+                  align="center"
+                  label="订单支付金额"
+                  min-width="135"
+                  prop="orderPayPrice"
+                  sortable
+                />
+              </el-table>
+            </el-col>
+          </el-row>
+        </el-card>
+      </el-col>
+      <el-col :md="6" :sm="24">
+        <el-card v-loading="loading" shadow="never">
+          <template #header>
+            <CardTitle title="会员性别比例" />
+          </template>
+          <Echart :height="300" :options="sexChartOptions" />
+        </el-card>
+      </el-col>
+    </el-row> -->
+  </div>
+</template>
+<script lang="ts" setup>
+import * as MemberStatisticsApi from '@/api/guide/statistics/member'
+import {
+  MemberAreaStatisticsRespVO,
+  MemberSexStatisticsRespVO,
+  MemberSummaryRespVO,
+  MemberTerminalStatisticsRespVO
+} from '@/api/guide/statistics/member'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { EChartsOption } from 'echarts'
+import china from '@/assets/map/json/china.json'
+import { areaReplace, fenToYuan } from '@/utils'
+import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
+import echarts from '@/plugins/echarts'
+import { fenToYuanFormat } from '@/utils/formatter'
+import MemberFunnelCard from './components/MemberFunnelCard.vue'
+import MemberTerminalCard from './components/MemberTerminalCard.vue'
+import { CardTitle } from '@/components/Card'
+
+/** 会员统计 */
+defineOptions({ name: 'MemberStatistics' })
+const { t } = useI18n()
+const loading = ref(true) // 加载中
+const summary = ref<MemberSummaryRespVO>() // 会员统计数据
+const areaStatisticsList = shallowRef<MemberAreaStatisticsRespVO[]>() // 省份会员统计
+
+// 注册地图
+echarts?.registerMap('china', china as any)
+
+/** 会员终端统计图配置 */
+// const terminalChartOptions = reactive<EChartsOption>({
+//   tooltip: {
+//     trigger: 'item',
+//     confine: true,
+//     formatter: '{a} <br/>{b} : {c} ({d}%)'
+//   },
+//   legend: {
+//     orient: 'vertical',
+//     left: 'right'
+//   },
+//   roseType: 'area',
+//   series: [
+//     {
+//       name: '会员终端',
+//       type: 'pie',
+//       label: {
+//         show: false
+//       },
+//       labelLine: {
+//         show: false
+//       },
+//       data: []
+//     }
+//   ]
+// }) as EChartsOption
+
+/** 会员性别统计图配置 */
+// const sexChartOptions = reactive<EChartsOption>({
+//   tooltip: {
+//     trigger: 'item',
+//     confine: true,
+//     formatter: '{a} <br/>{b} : {c} ({d}%)'
+//   },
+//   legend: {
+//     orient: 'vertical',
+//     left: 'right'
+//   },
+//   roseType: 'area',
+//   series: [
+//     {
+//       name: '会员性别',
+//       type: 'pie',
+//       label: {
+//         show: false
+//       },
+//       labelLine: {
+//         show: false
+//       },
+//       data: []
+//     }
+//   ]
+// }) as EChartsOption
+
+// const areaChartOptions = reactive<EChartsOption>({
+//   tooltip: {
+//     trigger: 'item',
+//     formatter: (params: any) => {
+//       return `${params?.data?.areaName || params?.name}<br/>
+// 会员数量:${params?.data?.userCount || 0}<br/>
+// 订单创建数量:${params?.data?.orderCreateUserCount || 0}<br/>
+// 订单支付数量:${params?.data?.orderPayUserCount || 0}<br/>
+// 订单支付金额:${fenToYuan(params?.data?.orderPayPrice || 0)}`
+//     }
+//   },
+//   visualMap: {
+//     text: ['高', '低'],
+//     realtime: false,
+//     calculable: true,
+//     top: 'middle',
+//     inRange: {
+//       color: ['#fff', '#3b82f6']
+//     }
+//   },
+//   series: [
+//     {
+//       name: '会员地域分布',
+//       type: 'map',
+//       map: 'china',
+//       roam: false,
+//       selectedMode: false,
+//       data: []
+//     }
+//   ]
+// }) as EChartsOption
+
+/** 查询会员统计 */
+const getMemberSummary = async () => {
+  summary.value = await MemberStatisticsApi.getMemberSummary()
+}
+
+/** 按照省份,查询会员统计列表 */
+// const getMemberAreaStatisticsList = async () => {
+//   const list = await MemberStatisticsApi.getMemberAreaStatisticsList()
+//   areaStatisticsList.value = list.map((item: MemberAreaStatisticsRespVO) => {
+//     return {
+//       ...item,
+//       areaName: areaReplace(item.areaName)
+//     }
+//   })
+//   let min = 0
+//   let max = 0
+//   areaChartOptions.series![0].data = areaStatisticsList.value.map((item) => {
+//     min = Math.min(min, item.orderPayUserCount || 0)
+//     max = Math.max(max, item.orderPayUserCount || 0)
+//     return { ...item, name: item.areaName, value: item.orderPayUserCount || 0 }
+//   })
+//   areaChartOptions.visualMap!['min'] = min
+//   areaChartOptions.visualMap!['max'] = max
+// }
+
+/** 按照性别,查询会员统计列表 */
+// const getMemberSexStatisticsList = async () => {
+//   const list = await MemberStatisticsApi.getMemberSexStatisticsList()
+//   const dictDataList = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)
+//   dictDataList.push({ label: '未知', value: null } as any)
+//   sexChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
+//     const userCount = list.find(
+//       (item: MemberSexStatisticsRespVO) => item.sex === dictData.value
+//     )?.userCount
+//     return {
+//       name: dictData.label,
+//       value: userCount || 0
+//     }
+//   })
+// }
+
+/** 按照终端,查询会员统计列表 */
+// const getMemberTerminalStatisticsList = async () => {
+//   const list = await MemberStatisticsApi.getMemberTerminalStatisticsList()
+//   const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
+//   dictDataList.push({ label: '未知', value: null } as any)
+//   terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
+//     const userCount = list.find(
+//       (item: MemberTerminalStatisticsRespVO) => item.terminal === dictData.value
+//     )?.userCount
+//     return {
+//       name: dictData.label,
+//       value: userCount || 0
+//     }
+//   })
+// }
+
+/** 初始化 **/
+onMounted(async () => {
+  loading.value = true
+  await Promise.all([
+    getMemberSummary(),
+    // getMemberTerminalStatisticsList(),
+    // getMemberAreaStatisticsList(),
+    // getMemberSexStatisticsList()
+  ])
+  loading.value = false
+})
+</script>
+<style lang="scss" scoped>
+.summary {
+  .el-col {
+    margin-bottom: 1rem;
+  }
+}
+</style>

+ 107 - 0
src/views/guide/statistics/sights/components/SightsRank.vue

@@ -0,0 +1,107 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <!-- 标题 -->
+      <div class="flex flex-row items-center justify-between">
+        <CardTitle :title="t('statistics.attraction_rank')" />
+        <!-- 查询条件 -->
+        <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="handleDateRangeChange" />
+      </div>
+    </template>
+    <!-- 排行列表 -->
+    <el-table v-loading="loading" :data="list" @sort-change="handleSortChange">
+      <el-table-column :label="t('statistics.attraction_id')" prop="sightsId" min-width="70" />
+      <el-table-column :label="t('statistics.attraction_image')" align="center" prop="picUrl" width="80">
+        <template #default="{ row }">
+          <el-image
+            :src="row.picUrl"
+            :preview-src-list="[row.picUrl]"
+            class="h-30px w-30px"
+            preview-teleported
+          />
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('statistics.attraction_name')" prop="sightsTitles" min-width="250" :show-overflow-tooltip="true" >
+        <template #default="{ row }">
+          <el-text v-for="(item, index) in row.sightsTitles" :key="index" style="display: block; margin-bottom: 1px;">
+            {{ item }} 
+          </el-text>
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('statistics.views')" prop="browseCount" min-width="90" sortable="custom" />
+      <el-table-column :label="t('statistics.visitors')" prop="browseUserCount" min-width="110" sortable="custom" />
+      <el-table-column :label="t('statistics.added_to_cart')" prop="cartCount" min-width="215" sortable="custom" />
+      <el-table-column :label="t('statistics.ordered_items')" prop="orderCount" min-width="115" sortable="custom" />
+      <el-table-column :label="t('statistics.paid_items')" prop="orderPayCount" min-width="145" sortable="custom" />
+      <el-table-column :label="t('statistics.payment_amount')" prop="orderPayPrice" min-width="105" sortable="custom" />
+      <el-table-column :label="t('statistics.favorites')" prop="favoriteCount" min-width="120" sortable="custom" />
+      <el-table-column
+        :label="t('statistics.conversion_rate')"
+        prop="browseConvertPercent"
+        min-width="180"
+        sortable="custom"
+        :formatter="formatConvertRate"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getSightsList"
+    />
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { SightsStatisticsApi, SightsStatisticsVO } from '@/api/guide/statistics/sights'
+import { CardTitle } from '@/components/Card'
+import { buildSortingField } from '@/utils'
+
+/** 商品排行 */
+defineOptions({ name: 'SightsRank' })
+const { t } = useI18n()
+// 格式化:访客-支付转化率
+const formatConvertRate = (row: SightsStatisticsVO) => {
+  return `${row.browseConvertPercent}%`
+}
+
+const handleSortChange = (params: any) => {
+  queryParams.sortingFields = [buildSortingField(params)]
+  getSightsList()
+}
+
+const handleDateRangeChange = (times: any[]) => {
+  queryParams.times = times as []
+  getSightsList()
+}
+
+const shortcutDateRangePicker = ref()
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  times: [],
+  sortingFields: {}
+})
+const loading = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref<SightsStatisticsVO[]>([]) // 列表的数据
+
+/** 查询商品列表 */
+const getSightsList = async () => {
+  loading.value = true
+  try {
+    const data = await SightsStatisticsApi.getSightsStatisticsRankPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getSightsList()
+})
+</script>
+<style lang="scss" scoped></style>

+ 294 - 0
src/views/guide/statistics/sights/components/SightsSummary.vue

@@ -0,0 +1,294 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <!-- 标题 -->
+      <div class="flex flex-row items-center justify-between">
+        <CardTitle :title="t('statistics.attraction_overview')" />
+        <!-- 查询条件 -->
+        <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getSightsTrendData">
+          <el-button
+            class="ml-4"
+            @click="handleExport"
+            :loading="exportLoading"
+            v-hasPermi="['statistics:product:export']"
+          >
+            <Icon icon="ep:download" class="mr-1" />{{ t('action.export') }}
+          </el-button>
+        </ShortcutDateRangePicker>
+      </div>
+    </template>
+    <!-- 统计值 -->
+    <el-row :gutter="16">
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.views')"
+          :tooltip="t('statistics.attraction_page_views')"
+          icon="ep:view"
+          icon-color="bg-blue-100"
+          icon-bg-color="text-blue-500"
+          :value="(trendSummary?.value?.browseCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.browseCount,
+              trendSummary?.reference?.browseCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.visitors')"
+          :tooltip="t('statistics.attraction_visitors')"
+          icon="ep:user-filled"
+          icon-color="bg-purple-100"
+          icon-bg-color="text-purple-500"
+          :value="(trendSummary?.value?.browseUserCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.browseUserCount,
+              trendSummary?.reference?.browseUserCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.paid_items')"
+          :tooltip="t('statistics.paid_items_count')"
+          icon="fa-solid:money-check-alt"
+          icon-color="bg-yellow-100"
+          icon-bg-color="text-yellow-500"
+          :value="(trendSummary?.value?.orderPayCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.orderPayCount,
+              trendSummary?.reference?.orderPayCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.payment_amount')"
+          :tooltip="t('statistics.payment_total')"
+          icon="ep:warning-filled"
+          icon-color="bg-green-100"
+          icon-bg-color="text-green-500"
+          prefix="¥"
+          :value="(trendSummary?.value?.orderPayPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.orderPayPrice,
+              trendSummary?.reference?.orderPayPrice
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.refund_items')"
+          :tooltip="t('statistics.refund_count')"
+          icon="fa-solid:wallet"
+          icon-color="bg-cyan-100"
+          icon-bg-color="text-cyan-500"
+          :value="(trendSummary?.value?.afterSaleCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.afterSaleCount,
+              trendSummary?.reference?.afterSaleCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.refund_amounts')"
+          :tooltip="t('statistics.refund_amount')"
+          icon="fa-solid:award"
+          icon-color="bg-yellow-100"
+          icon-bg-color="text-yellow-500"
+          prefix="¥"
+          :value="(trendSummary?.value?.afterSaleRefundPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.afterSaleRefundPrice,
+              trendSummary?.reference?.afterSaleRefundPrice
+            )
+          "
+        />
+      </el-col>
+    </el-row>
+    <!-- 折线图 -->
+    <el-skeleton :loading="trendLoading" animated>
+      <Echart :height="500" :options="lineChartOptions" />
+    </el-skeleton>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { SightsStatisticsApi, SightsStatisticsVO } from '@/api/guide/statistics/sights'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { EChartsOption } from 'echarts'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+import { calculateRelativeRate } from '@/utils'
+import download from '@/utils/download'
+import { CardTitle } from '@/components/Card'
+import * as DateUtil from '@/utils/formatTime'
+import dayjs from 'dayjs'
+
+/** 商品概况 */
+defineOptions({ name: 'SightsSummary' })
+const { t } = useI18n()
+const message = useMessage() // 消息弹窗
+
+const trendLoading = ref(true) // 商品状态加载中
+const exportLoading = ref(false) // 导出的加载中
+const trendSummary = ref<GuideDataComparisonRespVO<SightsStatisticsVO>>() // 商品状况统计数据
+const shortcutDateRangePicker = ref()
+
+/** 折线图配置 */
+const lineChartOptions = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['time', 'browseCount', 'browseUserCount', 'orderPayPrice', 'afterSaleRefundPrice'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+  { name: t('statistics.views'), type: 'line', smooth: true, itemStyle: { color: '#B37FEB' } },
+    { name: t('statistics.visitors'), type: 'line', smooth: true, itemStyle: { color: '#FFAB2B' } },
+    { name: t('statistics.payment_amount'), type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#1890FF' } },
+    { name: t('statistics.refund_amounts'), type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#00C050' } }
+  ],
+  toolbox: {
+    feature: {
+      // 数据区域缩放
+      dataZoom: {
+        yAxisIndex: false // Y轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '景点状况' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: true,
+    axisTick: {
+      show: false
+    }
+  },
+  yAxis: [
+    {
+      type: 'value',
+      name: t('statistics.quantity'),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        textStyle: {
+          color: '#7F8B9C'
+        }
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: '#F5F7F9'
+        }
+      }
+    },
+    {
+      type: 'value',
+      name: t('statistics.amount'),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        textStyle: {
+          color: '#7F8B9C'
+        }
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: '#F5F7F9'
+        }
+      }
+    }
+  ]
+}) as EChartsOption
+
+/** 处理商品状况查询 */
+const getSightsTrendData = async () => {
+  trendLoading.value = true
+  // 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
+  const times = shortcutDateRangePicker.value.times
+  if (DateUtil.isSameDay(times[0], times[1])) {
+    // 前天
+    times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
+  }
+  // 查询数据
+  await Promise.all([getSightsTrendSummary(), getSightsStatisticsList()])
+  trendLoading.value = false
+}
+
+/** 查询商品状况数据统计 */
+const getSightsTrendSummary = async () => {
+  const times = shortcutDateRangePicker.value.times
+  trendSummary.value = await SightsStatisticsApi.getSightsStatisticsAnalyse({ times })
+}
+
+/** 查询商品状况数据列表 */
+const getSightsStatisticsList = async () => {
+  // 查询数据
+  const times = shortcutDateRangePicker.value.times
+  const list: SightsStatisticsVO[] = await SightsStatisticsApi.getSightsStatisticsList({ times })
+  // 处理数据
+  for (let item of list) {
+    item.orderPayPrice = (item.orderPayPrice)
+    item.afterSaleRefundPrice = (item.afterSaleRefundPrice)
+  }
+  // 更新 Echarts 数据
+  if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+    lineChartOptions.dataset['source'] = list
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const times = shortcutDateRangePicker.value.times
+    const data = await SightsStatisticsApi.exportSightsStatisticsExcel({ times })
+    download.excel(data, t('statistics.attraction_status_excel'))
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+</script>
+<style lang="scss" scoped></style>

+ 16 - 0
src/views/guide/statistics/sights/index.vue

@@ -0,0 +1,16 @@
+<template>
+  <!-- <doc-alert title="【统计】会员、商品、交易统计" url="https://doc.iocoder.cn/mall/statistics/" /> -->
+
+  <!-- 商品概览 -->
+  <SightsSummary />
+  <!-- 商品排行 -->
+  <SightsRank class="mt-16px" />
+</template>
+<script lang="ts" setup>
+import SightsSummary from './components/SightsSummary.vue'
+import SightsRank from './components/SightsRank.vue'
+
+/** 商品统计 */
+defineOptions({ name: 'SightsStatistics' })
+</script>
+<style lang="scss" scoped></style>

+ 36 - 0
src/views/guide/statistics/trade/components/TradeStatisticValue.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="flex flex-col gap-2 bg-[var(--el-bg-color-overlay)] p-6">
+    <div class="flex items-center justify-between text-gray-500">
+      <span>{{ title }}</span>
+      <el-tooltip :content="tooltip" placement="top-start" v-if="tooltip">
+        <Icon icon="ep:warning" />
+      </el-tooltip>
+    </div>
+    <div class="mb-4 text-3xl">
+      <CountTo :prefix="prefix" :end-val="value" :decimals="decimals" />
+    </div>
+    <div class="flex flex-row gap-1 text-sm">
+      <span class="text-gray-500">环比</span>
+      <span :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'">
+        {{ Math.abs(toNumber(percent)) }}%
+        <Icon :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'" class="!text-sm" />
+      </span>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { toNumber } from 'lodash-es'
+
+/** 交易统计值组件 */
+defineOptions({ name: 'TradeStatisticValue' })
+
+defineProps({
+  tooltip: propTypes.string.def(''),
+  title: propTypes.string.def(''),
+  prefix: propTypes.string.def(''),
+  value: propTypes.number.def(0),
+  decimals: propTypes.number.def(0),
+  percent: propTypes.oneOfType([Number, String]).def(0)
+})
+</script>

+ 353 - 0
src/views/guide/statistics/trade/index.vue

@@ -0,0 +1,353 @@
+<template>
+  <!-- <doc-alert title="【统计】会员、商品、交易统计" url="https://doc.iocoder.cn/mall/statistics/" /> -->
+
+  <div class="flex flex-col">
+    <el-row :gutter="16" class="summary">
+      <el-col :sm="6" :xs="12">
+        <TradeStatisticValue
+          :tooltip="t('statistics.yesterday_order_count')"
+          :title="t('statistics.yesterday_order_count')"
+          :value="summary?.value?.yesterdayOrderCount || 0"
+          :percent="
+            calculateRelativeRate(
+              summary?.value?.yesterdayOrderCount,
+              summary?.reference?.yesterdayOrderCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :sm="6" :xs="12">
+        <TradeStatisticValue
+          :tooltip="t('statistics.this_month_order_count')"
+          :title="t('statistics.this_month_order_count')"
+          :value="summary?.value?.monthOrderCount || 0"
+          :percent="
+            calculateRelativeRate(
+              summary?.value?.monthOrderCount,
+              summary?.reference?.monthOrderCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :sm="6" :xs="12">
+        <TradeStatisticValue
+          :tooltip="t('statistics.yesterday_payment')"
+          :title="t('statistics.yesterday_payment')"
+          prefix="¥"
+          :value="(summary?.value?.yesterdayPayPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              summary?.value?.yesterdayPayPrice,
+              summary?.reference?.yesterdayPayPrice
+            )
+          "
+        />
+      </el-col>
+      <el-col :sm="6" :xs="12">
+        <TradeStatisticValue
+          :tooltip="t('statistics.this_month_payment')"
+          :title="t('statistics.this_month_payment')"
+          :value="(summary?.value?.monthPayPrice || 0)"
+          :percent="
+            calculateRelativeRate(summary?.value?.monthPayPrice, summary?.reference?.monthPayPrice)
+          "
+        />
+      </el-col>
+    </el-row>
+    <el-card shadow="never">
+      <template #header>
+        <!-- 标题 -->
+        <div class="flex flex-row items-center justify-between">
+          <CardTitle :title="t('statistics.transaction_status') " />
+          <!-- 查询条件 -->
+          <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getTradeTrendData">
+            <el-button
+              class="ml-4"
+              @click="handleExport"
+              :loading="exportLoading"
+              v-hasPermi="['statistics:trade:export']"
+            >
+              <Icon icon="ep:download" class="mr-1" />{{ t('action.export') }}
+            </el-button>
+          </ShortcutDateRangePicker>
+        </div>
+      </template>
+      <!-- 统计值 -->
+      <el-row :gutter="16">
+        <el-col :md="6" :sm="12" :xs="24">
+          <SummaryCard
+            :title="t('statistics.revenue')"
+            :tooltip="t('statistics.guide_booking_payment')"
+            icon="fa-solid:yen-sign"
+            icon-color="bg-blue-100"
+            icon-bg-color="text-blue-500"
+            prefix="¥"
+            :value="(trendSummary?.value?.turnoverPrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.turnoverPrice,
+                trendSummary?.reference?.turnoverPrice
+              )
+            "
+          />
+        </el-col>
+        <el-col :md="6" :sm="12" :xs="24">
+          <SummaryCard
+            :title="t('statistics.guide_booking_payment')"
+            :tooltip="t('statistics.actual_payment')"
+            icon="fa-solid:shopping-cart"
+            icon-color="bg-purple-100"
+            icon-bg-color="text-purple-500"
+            prefix="¥"
+            :value="(trendSummary?.value?.orderPayPrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.orderPayPrice,
+                trendSummary?.reference?.orderPayPrice
+              )
+            "
+          />
+        </el-col>
+        <!-- <el-col :md="6" :sm="12" :xs="24">
+          <SummaryCard
+            title="充值金额"
+            tooltip="用户成功充值的金额"
+            icon="fa-solid:money-check-alt"
+            icon-color="bg-yellow-100"
+            icon-bg-color="text-yellow-500"
+            prefix="¥"
+            :value="(trendSummary?.value?.rechargePrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.rechargePrice,
+                trendSummary?.reference?.rechargePrice
+              )
+            "
+          />
+        </el-col> -->
+        <!-- <el-col :md="6" :sm="12" :xs="24">
+          <SummaryCard
+            title="支出金额"
+            tooltip="余额支付金额、支付佣金金额、商品退款金额"
+            icon="ep:warning-filled"
+            icon-color="bg-green-100"
+            icon-bg-color="text-green-500"
+            prefix="¥"
+            :value="(trendSummary?.value?.expensePrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.expensePrice,
+                trendSummary?.reference?.expensePrice
+              )
+            "
+          />
+        </el-col> -->
+        <!-- <el-col :md="6" :sm="12" :xs="24">
+          <SummaryCard
+            title="余额支付金额"
+            tooltip="用户下单时使用余额实际支付的金额"
+            icon="fa-solid:wallet"
+            icon-color="bg-cyan-100"
+            icon-bg-color="text-cyan-500"
+            prefix="¥"
+            :value="(trendSummary?.value?.walletPayPrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.walletPayPrice,
+                trendSummary?.reference?.walletPayPrice
+              )
+            "
+          />
+        </el-col> -->
+        <!-- <el-col :md="6" :sm="12" :xs="24">
+          <SummaryCard
+            title="支付佣金金额"
+            tooltip="后台给推广员支付的推广佣金,以实际支付为准"
+            icon="fa-solid:award"
+            icon-color="bg-yellow-100"
+            icon-bg-color="text-yellow-500"
+            prefix="¥"
+            :value="(trendSummary?.value?.brokerageSettlementPrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.brokerageSettlementPrice,
+                trendSummary?.reference?.brokerageSettlementPrice
+              )
+            "
+          />
+        </el-col> -->
+        <el-col :md="6" :sm="12" :xs="24">
+          <SummaryCard
+            :title="t('statistics.total_refund')"
+            :tooltip="t('statistics.total_refund')"
+            icon="fa-solid:times-circle"
+            icon-color="bg-blue-100"
+            icon-bg-color="text-blue-500"
+            prefix="¥"
+            :value="(trendSummary?.value?.afterSaleRefundPrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.afterSaleRefundPrice,
+                trendSummary?.reference?.afterSaleRefundPrice
+              )
+            "
+          />
+        </el-col>
+      </el-row>
+      <!-- 折线图 -->
+      <el-skeleton :loading="trendLoading" animated>
+        <Echart :height="500" :options="lineChartOptions" />
+      </el-skeleton>
+    </el-card>
+  </div>
+</template>
+<script lang="ts" setup>
+import * as GuideTradeStatisticsApi from '@/api/guide/statistics/trade'
+import TradeStatisticValue from './components/TradeStatisticValue.vue'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { EChartsOption } from 'echarts'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+import { GuideTradeSummaryRespVO, GuideTradeTrendSummaryRespVO } from '@/api/guide/statistics/trade'
+import { calculateRelativeRate, fenToYuan } from '@/utils'
+import download from '@/utils/download'
+import { CardTitle } from '@/components/Card'
+import * as DateUtil from '@/utils/formatTime'
+import dayjs from 'dayjs'
+
+/** 交易统计 */
+defineOptions({ name: 'TradeStatistics' })
+const { t } = useI18n()
+const message = useMessage() // 消息弹窗
+
+const trendLoading = ref(true) // 交易状态加载中
+const exportLoading = ref(false) // 导出的加载中
+const summary = ref<GuideDataComparisonRespVO<GuideTradeSummaryRespVO>>() // 交易统计数据
+const trendSummary = ref<GuideDataComparisonRespVO<GuideTradeTrendSummaryRespVO>>() // 交易状况统计数据
+const shortcutDateRangePicker = ref()
+
+/** 折线图配置 */
+const lineChartOptions = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['date', 'turnoverPrice', 'orderPayPrice', 'rechargePrice', 'expensePrice'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+    { name: t('statistics.revenue'), type: 'line', smooth: true },
+    { name: t('statistics.payment_received'), type: 'line', smooth: true },
+    { name: t('statistics.recharge_amount'), type: 'line', smooth: true },
+    { name: t('statistics.expense_amount'), type: 'line', smooth: true }
+  ],
+  toolbox: {
+    feature: {
+      // 数据区域缩放
+      dataZoom: {
+        yAxisIndex: false // Y轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: t('statistics.transaction_status') } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: false,
+    axisTick: {
+      show: false
+    }
+  },
+  yAxis: {
+    axisTick: {
+      show: false
+    }
+  }
+}) as EChartsOption
+
+/** 处理交易状况查询 */
+const getTradeTrendData = async () => {
+  trendLoading.value = true
+  // 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
+  const times = shortcutDateRangePicker.value.times
+  if (DateUtil.isSameDay(times[0], times[1])) {
+    // 前天
+    times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
+  }
+  // 查询数据
+  await Promise.all([getTradeStatisticsAnalyse(), getTradeStatisticsList()])
+  trendLoading.value = false
+}
+
+/** 查询交易统计 */
+const getTradeStatisticsSummary = async () => {
+  summary.value = await GuideTradeStatisticsApi.getTradeStatisticsSummary()
+}
+
+/** 查询交易状况数据统计 */
+const getTradeStatisticsAnalyse = async () => {
+  const times = shortcutDateRangePicker.value.times
+  trendSummary.value = await GuideTradeStatisticsApi.getTradeStatisticsAnalyse({ times })
+}
+
+/** 查询交易状况数据列表 */
+const getTradeStatisticsList = async () => {
+  // 查询数据
+  const times = shortcutDateRangePicker.value.times
+  const list = await GuideTradeStatisticsApi.getTradeStatisticsList({ times })
+  // 处理数据
+  for (let item of list) {
+    item.turnoverPrice = (item.turnoverPrice)
+    item.orderPayPrice = (item.orderPayPrice)
+    item.rechargePrice = (item.rechargePrice)
+    item.expensePrice = (item.expensePrice)
+  }
+  // 更新 Echarts 数据
+  if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+    lineChartOptions.dataset['source'] = list
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const times = shortcutDateRangePicker.value.times
+    const data = await GuideTradeStatisticsApi.exportTradeStatisticsExcel({ times })
+    download.excel(data, t('statistics.transaction_status')+'.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getTradeStatisticsSummary()
+})
+</script>
+<style lang="scss" scoped>
+.summary {
+  .el-col {
+    margin-bottom: 1rem;
+  }
+}
+</style>

+ 107 - 0
src/views/guide/statistics/trip/components/TripRank.vue

@@ -0,0 +1,107 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <!-- 标题 -->
+      <div class="flex flex-row items-center justify-between">
+        <CardTitle :title="t('statistics.itinerary_rank')" />
+        <!-- 查询条件 -->
+        <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="handleDateRangeChange" />
+      </div>
+    </template>
+    <!-- 排行列表 -->
+    <el-table v-loading="loading" :data="list" @sort-change="handleSortChange">
+      <el-table-column :label="t('statistics.itinerary_id')" prop="tripId" min-width="70" />
+      <!-- <el-table-column label="行程图" align="center" prop="picUrl" width="80">
+        <template #default="{ row }">
+          <el-image
+            :src="row.picUrl"
+            :preview-src-list="[row.picUrl]"
+            class="h-30px w-30px"
+            preview-teleported
+          />
+        </template>
+      </el-table-column> -->
+      <el-table-column :label="t('statistics.attraction_name')" prop="tripTitle" min-width="250" :show-overflow-tooltip="true" >
+        <template #default="{ row }">
+          <el-text v-for="(item, index) in row.tripTitle" :key="index" style="display: block; margin-bottom: 1px;">
+            {{ item }} 
+          </el-text>
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('statistics.views')" prop="browseCount" min-width="90" sortable="custom" />
+      <el-table-column :label="t('statistics.visitors')" prop="browseUserCount" min-width="100" sortable="custom" />
+      <el-table-column :label="t('statistics.added_to_cart')" prop="cartCount" min-width="205" sortable="custom" />
+      <el-table-column :label="t('statistics.ordered_items')" prop="orderCount" min-width="105" sortable="custom" />
+      <el-table-column :label="t('statistics.paid_items')" prop="orderPayCount" min-width="145" sortable="custom" />
+      <el-table-column :label="t('statistics.payment_amount')" prop="orderPayPrice" min-width="105" sortable="custom" />
+      <el-table-column :label="t('statistics.favorites')" prop="favoriteCount" min-width="120" sortable="custom" />
+      <el-table-column
+        :label="t('statistics.conversion_rate')"
+        prop="browseConvertPercent"
+        min-width="180"
+        sortable="custom"
+        :formatter="formatConvertRate"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getTripList"
+    />
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { TripStatisticsApi, TripStatisticsVO } from '@/api/guide/statistics/trip'
+import { CardTitle } from '@/components/Card'
+import { buildSortingField } from '@/utils'
+
+/** 商品排行 */
+defineOptions({ name: 'TripRank' })
+const { t } = useI18n()
+// 格式化:访客-支付转化率
+const formatConvertRate = (row: TripStatisticsVO) => {
+  return `${row.browseConvertPercent}%`
+}
+
+const handleSortChange = (params: any) => {
+  queryParams.sortingFields = [buildSortingField(params)]
+  getTripList()
+}
+
+const handleDateRangeChange = (times: any[]) => {
+  queryParams.times = times as []
+  getTripList()
+}
+
+const shortcutDateRangePicker = ref()
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  times: [],
+  sortingFields: {}
+})
+const loading = ref(false) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref<TripStatisticsVO[]>([]) // 列表的数据
+
+/** 查询商品列表 */
+const getTripList = async () => {
+  loading.value = true
+  try {
+    const data = await TripStatisticsApi.getTripStatisticsRankPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getTripList()
+})
+</script>
+<style lang="scss" scoped></style>

+ 294 - 0
src/views/guide/statistics/trip/components/TripSummary.vue

@@ -0,0 +1,294 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <!-- 标题 -->
+      <div class="flex flex-row items-center justify-between">
+        <CardTitle :title="t('statistics.itinerary_overview')" />
+        <!-- 查询条件 -->
+        <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getTripTrendData">
+          <el-button
+            class="ml-4"
+            @click="handleExport"
+            :loading="exportLoading"
+            v-hasPermi="['statistics:product:export']"
+          >
+            <Icon icon="ep:download" class="mr-1" />{{ t('action.export') }}
+          </el-button>
+        </ShortcutDateRangePicker>
+      </div>
+    </template>
+    <!-- 统计值 -->
+    <el-row :gutter="16">
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.views')"
+          :tooltip="t('statistics.itinerary_page_views')"
+          icon="ep:view"
+          icon-color="bg-blue-100"
+          icon-bg-color="text-blue-500"
+          :value="(trendSummary?.value?.browseCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.browseCount,
+              trendSummary?.reference?.browseCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.visitors')"
+          :tooltip="t('statistics.itinerary_visitors')"
+          icon="ep:user-filled"
+          icon-color="bg-purple-100"
+          icon-bg-color="text-purple-500"
+          :value="(trendSummary?.value?.browseUserCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.browseUserCount,
+              trendSummary?.reference?.browseUserCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.paid_items')"
+          :tooltip="t('statistics.paid_items_count')"
+          icon="fa-solid:money-check-alt"
+          icon-color="bg-yellow-100"
+          icon-bg-color="text-yellow-500"
+          :value="(trendSummary?.value?.orderPayCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.orderPayCount,
+              trendSummary?.reference?.orderPayCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.payment_amount')"
+          :tooltip="t('statistics.payment_total')"
+          icon="ep:warning-filled"
+          icon-color="bg-green-100"
+          icon-bg-color="text-green-500"
+          prefix="¥"
+          :value="(trendSummary?.value?.orderPayPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.orderPayPrice,
+              trendSummary?.reference?.orderPayPrice
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.refund_items')"
+          :tooltip="t('statistics.refund_count')"
+          icon="fa-solid:wallet"
+          icon-color="bg-cyan-100"
+          icon-bg-color="text-cyan-500"
+          :value="(trendSummary?.value?.afterSaleCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.afterSaleCount,
+              trendSummary?.reference?.afterSaleCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          :title="t('statistics.refund_amounts')"
+          :tooltip="t('statistics.refund_amount')"
+          icon="fa-solid:award"
+          icon-color="bg-yellow-100"
+          icon-bg-color="text-yellow-500"
+          prefix="¥"
+          :value="(trendSummary?.value?.afterSaleRefundPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.afterSaleRefundPrice,
+              trendSummary?.reference?.afterSaleRefundPrice
+            )
+          "
+        />
+      </el-col>
+    </el-row>
+    <!-- 折线图 -->
+    <el-skeleton :loading="trendLoading" animated>
+      <Echart :height="500" :options="lineChartOptions" />
+    </el-skeleton>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { TripStatisticsApi, TripStatisticsVO } from '@/api/guide/statistics/trip'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { EChartsOption } from 'echarts'
+import { GuideDataComparisonRespVO } from '@/api/guide/statistics/common'
+import { calculateRelativeRate } from '@/utils'
+import download from '@/utils/download'
+import { CardTitle } from '@/components/Card'
+import * as DateUtil from '@/utils/formatTime'
+import dayjs from 'dayjs'
+
+/** 商品概况 */
+defineOptions({ name: 'TripSummary' })
+const { t } = useI18n()
+const message = useMessage() // 消息弹窗
+
+const trendLoading = ref(true) // 商品状态加载中
+const exportLoading = ref(false) // 导出的加载中
+const trendSummary = ref<GuideDataComparisonRespVO<TripStatisticsVO>>() // 商品状况统计数据
+const shortcutDateRangePicker = ref()
+
+/** 折线图配置 */
+const lineChartOptions = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['time', 'browseCount', 'browseUserCount', 'orderPayPrice', 'afterSaleRefundPrice'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+  { name: t('statistics.views'), type: 'line', smooth: true, itemStyle: { color: '#B37FEB' } },
+    { name: t('statistics.visitors'), type: 'line', smooth: true, itemStyle: { color: '#FFAB2B' } },
+    { name: t('statistics.payment_amount'), type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#1890FF' } },
+    { name: t('statistics.refund_amounts'), type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#00C050' } }
+  ],
+  toolbox: {
+    feature: {
+      // 数据区域缩放
+      dataZoom: {
+        yAxisIndex: false // Y轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '行程状况' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: true,
+    axisTick: {
+      show: false
+    }
+  },
+  yAxis: [
+    {
+      type: 'value',
+      name: t('statistics.quantity'),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        textStyle: {
+          color: '#7F8B9C'
+        }
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: '#F5F7F9'
+        }
+      }
+    },
+    {
+      type: 'value',
+      name: t('statistics.amount'),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        textStyle: {
+          color: '#7F8B9C'
+        }
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: '#F5F7F9'
+        }
+      }
+    }
+  ]
+}) as EChartsOption
+
+/** 处理商品状况查询 */
+const getTripTrendData = async () => {
+  trendLoading.value = true
+  // 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
+  const times = shortcutDateRangePicker.value.times
+  if (DateUtil.isSameDay(times[0], times[1])) {
+    // 前天
+    times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
+  }
+  // 查询数据
+  await Promise.all([getTripTrendSummary(), getTripStatisticsList()])
+  trendLoading.value = false
+}
+
+/** 查询商品状况数据统计 */
+const getTripTrendSummary = async () => {
+  const times = shortcutDateRangePicker.value.times
+  trendSummary.value = await TripStatisticsApi.getTripStatisticsAnalyse({ times })
+}
+
+/** 查询商品状况数据列表 */
+const getTripStatisticsList = async () => {
+  // 查询数据
+  const times = shortcutDateRangePicker.value.times
+  const list: TripStatisticsVO[] = await TripStatisticsApi.getTripStatisticsList({ times })
+  // 处理数据
+  for (let item of list) {
+    item.orderPayPrice = (item.orderPayPrice)
+    item.afterSaleRefundPrice = (item.afterSaleRefundPrice)
+  }
+  // 更新 Echarts 数据
+  if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+    lineChartOptions.dataset['source'] = list
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const times = shortcutDateRangePicker.value.times
+    const data = await TripStatisticsApi.exportTripStatisticsExcel({ times })
+    download.excel(data, t('statistics.itinerary_status_excel'))
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+</script>
+<style lang="scss" scoped></style>

+ 16 - 0
src/views/guide/statistics/trip/index.vue

@@ -0,0 +1,16 @@
+<template>
+  <!-- <doc-alert title="【统计】会员、商品、交易统计" url="https://doc.iocoder.cn/mall/statistics/" /> -->
+
+  <!-- 商品概览 -->
+  <TripSummary />
+  <!-- 商品排行 -->
+  <TripRank class="mt-16px" />
+</template>
+<script lang="ts" setup>
+import TripSummary from './components/TripSummary.vue'
+import TripRank from './components/TripRank.vue'
+
+/** 商品统计 */
+defineOptions({ name: 'TripStatistics' })
+</script>
+<style lang="scss" scoped></style>

+ 325 - 0
src/views/guide/trade/afterSale/detail/index.vue

@@ -0,0 +1,325 @@
+<template>
+  <ContentWrap>
+    <!-- 订单信息 -->
+    <el-descriptions :title="t('aftersale.orderInfo')">
+      <el-descriptions-item :label="t('aftersale.orderNo')">{{ formData.orderNo }}</el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.orderType')">
+        <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="formData.order.type" />
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.buyerMessage')">
+        {{ formData.order.userRemark }}
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.orderSource')">
+        <dict-tag :type="DICT_TYPE.TERMINAL" :value="formData.order.terminal" />
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.contactPhone')">
+        {{ formData.order.receiverMobile }}
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.merchantRemark')">{{ formData.order.remark }}</el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.paymentNumber')">
+        {{ formData.order.payOrderId }}
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.paymentMethod')">
+        <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="formData.order.payChannelCode" />
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.buyer')">{{ formData?.user?.nickname }}</el-descriptions-item>
+    </el-descriptions>
+
+    <!-- 售后信息 -->
+    <el-descriptions :title="t('aftersale.afterSalesInfo')">
+      <el-descriptions-item :label="t('aftersale.refundNumberLabel')">{{ formData.no }}</el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.applicationTime')">
+        {{ formatDate(formData.auditTime) }}
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.afterSalesType')">
+        <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_TYPE" :value="formData.type" />
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.afterSalesMethod')">
+        <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="formData.way" />
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.refundAmount')">
+        {{ (formData.refundPrice) }}円
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.refundReason')">{{ formData.applyReason }}</el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.additionalDescription')">
+        {{ formData.applyDescription }}
+      </el-descriptions-item>
+      <el-descriptions-item :label="t('aftersale.voucherImage')">
+        <el-image
+          v-for="(item, index) in formData.applyPicUrls"
+          :key="index"
+          :src="item.url"
+          class="mr-10px h-60px w-60px"
+          @click="imagePreview(formData.applyPicUrls)"
+        />
+      </el-descriptions-item>
+    </el-descriptions>
+
+    <!-- 退款状态 -->
+    <el-descriptions :column="1" :title="t('aftersale.refundStatus')">
+      <el-descriptions-item :label="t('aftersale.refundStatus')">
+        <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="formData.status" />
+      </el-descriptions-item>
+      <el-descriptions-item label-class-name="no-colon">
+        <el-button v-if="formData.status === 10" type="primary" @click="agree">{{t('aftersale.agreeRefund')}}</el-button>
+        <el-button v-if="formData.status === 10" type="primary" @click="disagree">
+          {{t('aftersale.denyRefund')}}
+        </el-button>
+        <!-- <el-button v-if="formData.status === 30" type="primary" @click="receive">
+          确认收货
+        </el-button>
+        <el-button v-if="formData.status === 30" type="primary" @click="refuse">拒绝收货</el-button> -->
+        <el-button v-if="formData.status === 40" type="primary" @click="refund">{{t('aftersale.confirmRefund')}}</el-button>
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label><span style="color: red">{{t('aftersale.reminder')}}</span></template>
+        {{t('aftersale.guideDateReminder')}}<br />
+        {{t('aftersale.expiredReminder')}}<br />
+      
+      </el-descriptions-item>
+    </el-descriptions>
+
+    <!-- 商品信息 -->
+    <el-descriptions :title="t('aftersale.reserveGuideInfo')">
+      <el-descriptions-item labelClassName="no-colon">
+        <el-row :gutter="20">
+          <el-col :span="15">
+            <el-table :data="[formData.orderItem]" border>
+              <el-table-column :label="t('aftersale.reservationInfo')" prop="spuName" width="auto">
+                <template #default="{ row }">
+                  <el-image
+                    :src="row.picUrl"
+                    class="mr-10px h-30px w-30px"
+                    @click="imagePreview(row.picUrl)"
+                  />
+                  <span class="mr-10px">{{ row.tripTitle }}</span>
+                  <el-tag  class="mr-10px">
+                    {{ row.guideName }}: {{ formatDate(row.bookingDate,'YYYY-MM-DD') }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column :label="t('aftersale.reservationPrice')" prop="price" width="150">
+                <template #default="{ row }">{{ (row.price) }} {{t('aftersale.yen')}}</template>
+              </el-table-column>
+            </el-table>
+          </el-col>
+          <el-col :span="10" />
+        </el-row>
+      </el-descriptions-item>
+    </el-descriptions>
+
+    <!-- 操作日志 -->
+    <el-descriptions :title="t('aftersale.afterSalesLog')">
+      <el-descriptions-item labelClassName="no-colon">
+        <el-timeline>
+          <el-timeline-item
+            v-for="saleLog in formData.logs"
+            :key="saleLog.id"
+            :timestamp="formatDate(saleLog.createTime)"
+            placement="top"
+          >
+            <div class="el-timeline-right-content">
+              <span>{{ saleLog.content }}</span>
+            </div>
+            <template #dot>
+              <span
+                :style="{ backgroundColor: getUserTypeColor(saleLog.userType) }"
+                class="dot-node-style"
+              >
+                {{ getDictLabel(DICT_TYPE.USER_TYPE, saleLog.userType)[0] || t('aftersale.system') }}
+              </span>
+            </template>
+          </el-timeline-item>
+        </el-timeline>
+      </el-descriptions-item>
+    </el-descriptions>
+  </ContentWrap>
+
+  <!-- 各种操作的弹窗 -->
+  <UpdateAuditReasonForm ref="updateAuditReasonFormRef" @success="getDetail" />
+</template>
+<script lang="ts" setup>
+import * as GuideAfterSaleApi from '@/api/guide/trade/afterSale/index'
+import { fenToYuan } from '@/utils'
+import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import UpdateAuditReasonForm from '@/views/guide/trade/afterSale/form/AfterSaleDisagreeForm.vue'
+import { createImageViewer } from '@/components/ImageViewer'
+import { isArray } from '@/utils/is'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+defineOptions({ name: 'TradeAfterSaleDetail' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const { params } = useRoute() // 查询参数
+const { push, currentRoute } = useRouter() // 路由
+const formData = ref({
+  order: {},
+  logs: []
+})
+const updateAuditReasonFormRef = ref() // 拒绝售后表单 Ref
+
+/** 获得 userType 颜色 */
+const getUserTypeColor = (type: number) => {
+  const dict = getDictObj(DICT_TYPE.USER_TYPE, type)
+  switch (dict?.colorType) {
+    case 'success':
+      return '#67C23A'
+    case 'info':
+      return '#909399'
+    case 'warning':
+      return '#E6A23C'
+    case 'danger':
+      return '#F56C6C'
+  }
+  return '#409EFF'
+}
+
+/** 获得详情 */
+const getDetail = async () => {
+  const id = params.id as unknown as number
+  if (id) {
+    const res = await GuideAfterSaleApi.getAfterSale(id)
+    // 没有表单信息则关闭页面返回
+    if (res == null) {
+      message.notifyError(t('aftersale.afterSalesOrderNotFound'))
+      close()
+    }
+    formData.value = res
+  }
+}
+
+/** 同意售后 */
+const agree = async () => {
+  try {
+    // 二次确认
+    await message.confirm(t('aftersale.agreeRefundPrompt'))
+    await GuideAfterSaleApi.agree(formData.value.id)
+    // 提示成功
+    message.success(t('common.success'))
+    await getDetail()
+  } catch {}
+}
+
+/** 拒绝售后 */
+const disagree = async () => {
+  updateAuditReasonFormRef.value?.open(formData.value)
+}
+
+/** 确认退款 */
+const refund = async () => {
+  try {
+    // 二次确认
+    await message.confirm(t('aftersale.confirmRefundPrompt'))
+    await GuideAfterSaleApi.refund(formData.value.id)
+    // 提示成功
+    message.success(t('common.success'))
+    await getDetail()
+  } catch {}
+}
+
+/** 图片预览 */
+const imagePreview = (args) => {
+  const urlList = []
+  if (isArray(args)) {
+    args.forEach((item) => {
+      urlList.push(item.url)
+    })
+  } else {
+    urlList.push(args)
+  }
+  createImageViewer({
+    urlList
+  })
+}
+const { delView } = useTagsViewStore() // 视图操作
+/** 关闭 tag */
+const close = () => {
+  delView(unref(currentRoute))
+  push({ name: 'TradeAfterSale' })
+}
+onMounted(async () => {
+  await getDetail()
+})
+</script>
+<style lang="scss" scoped>
+:deep(.el-descriptions) {
+  &:not(:nth-child(1)) {
+    margin-top: 20px;
+  }
+
+  .el-descriptions__title {
+    display: flex;
+    align-items: center;
+
+    &::before {
+      display: inline-block;
+      width: 3px;
+      height: 20px;
+      margin-right: 10px;
+      background-color: #409eff;
+      content: '';
+    }
+  }
+
+  .el-descriptions-item__container {
+    margin: 0 10px;
+
+    .no-colon {
+      margin: 0;
+
+      &::after {
+        content: '';
+      }
+    }
+  }
+}
+
+// 时间线样式调整
+:deep(.el-timeline) {
+  margin: 10px 0 0 160px;
+
+  .el-timeline-item__wrapper {
+    position: relative;
+    top: -20px;
+
+    .el-timeline-item__timestamp {
+      position: absolute !important;
+      top: 10px;
+      left: -150px;
+    }
+  }
+
+  .el-timeline-right-content {
+    display: flex;
+    align-items: center;
+    min-height: 30px;
+    padding: 10px;
+    background-color: #f7f8fa;
+
+    &::before {
+      position: absolute;
+      top: 10px;
+      left: 13px;
+      border-color: transparent #f7f8fa transparent transparent; /* 尖角颜色,左侧朝向 */
+      border-style: solid;
+      border-width: 8px; /* 调整尖角大小 */
+      content: '';
+    }
+  }
+
+  .dot-node-style {
+    position: absolute;
+    left: -5px;
+    display: flex;
+    width: 20px;
+    height: 20px;
+    font-size: 10px;
+    color: #fff;
+    border-radius: 50%;
+    justify-content: center;
+    align-items: center;
+  }
+}
+</style>

+ 70 - 0
src/views/guide/trade/afterSale/form/AfterSaleDisagreeForm.vue

@@ -0,0 +1,70 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="t('aftersale.denyAfterSales')" width="45%">
+    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+      <el-form-item :label="t('aftersale.approvalRemark')">
+        <el-input
+          v-model="formData.auditReason"
+          :rows="3"
+          :placeholder="t('aftersale.enterApprovalRemark')"
+          type="textarea"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">{{t('guide.common.confirm')}}</el-button>
+      <el-button @click="dialogVisible = false">{{t('guide.common.cancel')}}</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as GuideAfterSaleApi from '@/api/guide/trade/afterSale/index'
+
+defineOptions({ name: 'AfterSaleDisagreeForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: 0, // 售后订单编号
+  auditReason: '' // 审批备注
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (row: GuideAfterSaleApi.GuideTradeAfterSaleVO) => {
+  resetForm()
+  // 设置数据
+  formData.value.id = row.id
+  formData.value.auditReason = row.auditReason
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = unref(formData)
+    await GuideAfterSaleApi.disagree(data)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success', true)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: 0, // 售后订单编号
+    auditReason: '' // 审批备注
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 267 - 0
src/views/guide/trade/afterSale/index.vue

@@ -0,0 +1,267 @@
+<template>
+  <!-- <doc-alert title="【交易】售后退款" url="https://doc.iocoder.cn/mall/trade-aftersale/" /> -->
+
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form ref="queryFormRef" :inline="true" :model="queryParams" label-width="68px">
+      <el-form-item :label="t('aftersale.guideName')" prop="guideName">
+        <el-input
+          v-model="queryParams.spuName"
+          class="!w-280px"
+          clearable
+          :placeholder="t('aftersale.enterGuideName')"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item :label="t('aftersale.refundNumber')" prop="no">
+        <el-input
+          v-model="queryParams.no"
+          class="!w-280px"
+          clearable
+          :placeholder="t('aftersale.enterRefundNumber')"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item :label="t('aftersale.orderNumber')" prop="orderNo">
+        <el-input
+          v-model="queryParams.orderNo"
+          class="!w-280px"
+          clearable
+          :placeholder="t('aftersale.enterOrderNumber')"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item :label="t('aftersale.afterSalesStatus')" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          class="!w-280px"
+          clearable
+          :placeholder="t('aftersale.selectAfterSalesStatus')" 
+        >
+          <el-option :label="t('aftersale.all')" value="0" />
+          <el-option
+            v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="t('aftersale.afterSalesMethod')" prop="way">
+        <el-select
+          v-model="queryParams.way"
+          class="!w-280px"
+          clearable
+          :placeholder="t('aftersale.selectAfterSalesMethod')"
+        >
+          <el-option
+            v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_WAY)"
+            :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 getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item> -->
+      <el-form-item :label="t('aftersale.creationTime')" 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-260px"
+          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" />
+          {{t('guide.common.search')}}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          {{t('guide.common.reset')}}
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <el-tabs v-model="queryParams.status" @tab-click="tabClick">
+      <el-tab-pane
+        v-for="item in statusTabs"
+        :key="item.label"
+        :label="item.label"
+        :name="item.value"
+      />
+    </el-tabs>
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" :label="t('aftersale.refundNumber')" min-width="200" prop="no" />
+      <el-table-column align="center" :label="t('aftersale.orderNumber')" min-width="200" prop="orderNo">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="openOrderDetail(row.orderId)">
+            {{ row.orderNo }}
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column :label="t('aftersale.appointmentInfo')" min-width="500" prop="guideId">
+        <template #default="{ row }">
+          <div class="flex items-center">
+            <el-image
+              :src="row.picUrl"
+              class="mr-10px h-30px w-30px"
+              @click="imagePreview(row.picUrl)"
+            />
+            <span class="mr-10px">{{ row.tripTitle }}</span>
+            <el-tag  class="mr-10px">
+              {{ row.guideName }}: {{ formatDate(row.bookingDate,'YYYY-MM-DD') }}
+            </el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" :label="t('aftersale.orderAmount')" prop="refundPrice">
+        <template #default="scope">
+          <span>{{ (scope.row.refundPrice) }} {{t('aftersale.yen')}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" :label="t('aftersale.buyer')" prop="user.nickname" />
+      <el-table-column align="center" :label="t('aftersale.applicationTime')" prop="createTime" width="180">
+        <template #default="scope">
+          <span>{{ formatDate(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" :label="t('aftersale.afterSalesStatus')" width="200">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" :label="t('aftersale.afterSalesMethod')">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="scope.row.way" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" :label="t('guide.common.actions')" width="160">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="openAfterSaleDetail(row.id)">{{t('aftersale.processRefund')}}</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 lang="ts" setup>
+import * as GuideAfterSaleApi from '@/api/guide/trade/afterSale/index'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { createImageViewer } from '@/components/ImageViewer'
+import { TabsPaneContext } from 'element-plus'
+import { cloneDeep } from 'lodash-es'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'TradeAfterSale' })
+const { t } = useI18n() // 国际化
+const { push } = useRouter() // 路由跳转
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref<GuideAfterSaleApi.GuideTradeAfterSaleVO[]>([]) // 列表的数据
+const statusTabs = ref([
+  {
+    label: t('aftersale.all'),
+    value: '0'
+  }
+])
+const queryFormRef = ref() // 搜索的表单
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  no: null,
+  status: '0',
+  orderNo: null,
+  spuName: null,
+  createTime: [],
+  way: null,
+  type: null
+})
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = cloneDeep(queryParams)
+    // 处理掉全部的状态,不传就是全部
+    if (data.status === '0') {
+      delete data.status
+    }
+    // 执行查询
+    const res = (await GuideAfterSaleApi.getAfterSalePage(data)) as GuideAfterSaleApi.TradeAfterSaleVO[]
+    list.value = res.list
+    total.value = res.total
+  } finally {
+    loading.value = false
+  }
+}
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  queryParams.pageNo = 1
+  await getList()
+}
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+/** tab 切换 */
+const tabClick = async (tab: TabsPaneContext) => {
+  queryParams.status = tab.paneName
+  await getList()
+}
+
+/** 处理退款 */
+const openAfterSaleDetail = (id: number) => {
+  push({ name: 'GuideTradeAfterSaleDetail', params: { id } })
+}
+
+/** 查看订单详情 */
+const openOrderDetail = (id: number) => {
+  push({ name: 'GuideTradeOrderDetail', params: { id } })
+}
+
+/** 商品图预览 */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    urlList: [imgUrl]
+  })
+}
+
+onMounted(async () => {
+  await getList()
+  // 设置 statuses 过滤
+  for (const dict of getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)) {
+    statusTabs.value.push({
+      label: dict.label,
+      value: dict.value
+    })
+  }
+})
+</script>

+ 171 - 0
src/views/guide/trade/brokerage/record/index.vue

@@ -0,0 +1,171 @@
+<template>
+  <doc-alert title="【交易】分销返佣" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="用户编号" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          placeholder="请输入用户编号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <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.BROKERAGE_RECORD_BIZ_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_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"
+          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="60" />
+      <el-table-column label="用户编号" align="center" prop="userId" min-width="80" />
+      <el-table-column label="头像" align="center" prop="userAvatar" width="70px">
+        <template #default="scope">
+          <el-avatar :src="scope.row.userAvatar" />
+        </template>
+      </el-table-column>
+      <el-table-column label="昵称" align="center" prop="userNickname" min-width="80px" />
+      <el-table-column label="业务类型" align="center" prop="bizType" min-width="85">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE" :value="scope.row.bizType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="业务编号" align="center" prop="bizId" min-width="80" />
+      <el-table-column label="标题" align="center" prop="title" min-width="110" />
+      <el-table-column
+        label="金额"
+        align="center"
+        prop="price"
+        min-width="60"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column label="说明" align="center" prop="description" min-width="120" />
+      <el-table-column label="状态" align="center" prop="status" min-width="85">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="解冻时间"
+        align="center"
+        prop="unfreezeTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageRecordApi from '@/api/mall/trade/brokerage/record'
+import { fenToYuanFormat } from '@/utils/formatter'
+
+defineOptions({ name: 'TradeBrokerageRecord' })
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: null,
+  bizType: null,
+  price: null,
+  status: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BrokerageRecordApi.getBrokerageRecordPage(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>

+ 152 - 0
src/views/guide/trade/brokerage/user/BrokerageOrderListDialog.vue

@@ -0,0 +1,152 @@
+<template>
+  <Dialog v-model="dialogVisible" title="推广人列表" width="75%">
+    <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="status">
+          <el-select
+            v-model="queryParams.status"
+            placeholder="请选择状态"
+            clearable
+            class="!w-240px"
+          >
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_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"
+            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="bizId" min-width="80px" />
+        <el-table-column label="用户编号" align="center" prop="sourceUserId" min-width="80px" />
+        <el-table-column label="头像" align="center" prop="sourceUserAvatar" width="70px">
+          <template #default="scope">
+            <el-avatar :src="scope.row.sourceUserAvatar" />
+          </template>
+        </el-table-column>
+        <el-table-column label="昵称" align="center" prop="sourceUserNickname" min-width="80px" />
+        <el-table-column
+          label="佣金"
+          align="center"
+          prop="price"
+          min-width="100px"
+          :formatter="fenToYuanFormat"
+        />
+        <el-table-column label="状态" align="center" prop="status" min-width="85">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_STATUS" :value="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="创建时间"
+          align="center"
+          prop="createTime"
+          :formatter="dateFormatter"
+          width="180px"
+        />
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageRecordApi from '@/api/mall/trade/brokerage/record'
+import { BrokerageRecordBizTypeEnum } from '@/utils/constants'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+/** 推广订单列表 */
+defineOptions({ name: 'BrokerageOrderListDialog' })
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: null,
+  bizType: BrokerageRecordBizTypeEnum.ORDER.type,
+  level: '',
+  createTime: [],
+  status: null
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 打开弹窗 */
+const dialogVisible = ref(false) // 弹窗的是否展示
+const open = async (userId: any) => {
+  dialogVisible.value = true
+  queryParams.userId = userId
+  resetQuery()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BrokerageRecordApi.getBrokerageRecordPage(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()
+}
+</script>

+ 137 - 0
src/views/guide/trade/brokerage/user/BrokerageUserListDialog.vue

@@ -0,0 +1,137 @@
+<template>
+  <Dialog v-model="dialogVisible" title="推广人列表" width="75%">
+    <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="brokerageUserCount"
+          min-width="80px"
+        />
+        <el-table-column
+          label="推广订单数量"
+          align="center"
+          prop="brokerageOrderCount"
+          min-width="110px"
+        />
+        <el-table-column label="推广资格" align="center" prop="brokerageEnabled" min-width="80px">
+          <template #default="scope">
+            <el-tag v-if="scope.row.brokerageEnabled">有</el-tag>
+            <el-tag v-else type="info">无</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="绑定时间"
+          align="center"
+          prop="bindUserTime"
+          :formatter="dateFormatter"
+          width="180px"
+        />
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+
+/** 推广人列表 */
+defineOptions({ name: 'BrokerageUserListDialog' })
+
+const message = useMessage() // 消息弹窗
+
+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 dialogVisible = ref(false) // 弹窗的是否展示
+const open = async (bindUserId: any) => {
+  dialogVisible.value = true
+  queryParams.bindUserId = bindUserId
+  resetQuery()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    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()
+}
+</script>

+ 127 - 0
src/views/guide/trade/brokerage/user/UpdateBindUserForm.vue

@@ -0,0 +1,127 @@
+<template>
+  <Dialog v-model="dialogVisible" title="修改上级推广人" width="500">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-form-item label="推广人" prop="bindUserId">
+        <el-input
+          v-model="formData.bindUserId"
+          placeholder="请输入推广员编号"
+          v-loading="formLoading"
+        >
+          <template #append>
+            <el-button @click="handleGetUser"><Icon icon="ep:search" class="mr-5px" /></el-button>
+          </template>
+        </el-input>
+      </el-form-item>
+    </el-form>
+    <!-- 展示上级推广人的信息 -->
+    <el-descriptions v-if="bindUser" :column="1" border>
+      <el-descriptions-item label="头像">
+        <el-avatar :src="bindUser.avatar" />
+      </el-descriptions-item>
+      <el-descriptions-item label="昵称">{{ bindUser.nickname }}</el-descriptions-item>
+      <el-descriptions-item label="推广资格">
+        <el-tag v-if="bindUser.brokerageEnabled">有</el-tag>
+        <el-tag v-else type="info">无</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="成为推广员的时间">
+        {{ formatDate(bindUser.brokerageTime) }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+import { formatDate } from '@/utils/formatTime'
+
+/** 修改上级推广人表单 */
+defineOptions({ name: 'UpdateBindUserForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref()
+const formRef = ref() // 表单 Ref
+const formRules = reactive({
+  bindUserId: [{ required: true, message: '推广员人不能为空', trigger: 'blur' }]
+})
+
+/** 打开弹窗 */
+const open = async (row: BrokerageUserApi.BrokerageUserVO) => {
+  resetForm()
+  // 设置数据
+  formData.value.id = row.id
+  formData.value.bindUserId = row.bindUserId
+  // 反显上级推广人
+  if (row.bindUserId) {
+    await handleGetUser()
+  }
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+/** 修改上级推广人 */
+const submitForm = async () => {
+  if (formLoading.value) return
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 未查找到合适的上级
+  if (!bindUser.value) {
+    message.error('请先查询并确认推广人')
+    return
+  }
+
+  // 提交请求
+  formLoading.value = true
+  try {
+    // 发起修改
+    await BrokerageUserApi.updateBindUser(formData.value)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success', true)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: 0,
+    bindUserId: undefined
+  }
+  formRef.value?.resetFields()
+  bindUser.value = undefined
+}
+
+/** 查询推广员 */
+const bindUser = ref<BrokerageUserApi.BrokerageUserVO>()
+const handleGetUser = async () => {
+  if (formData.value.bindUserId == formData.value.id) {
+    message.error('不能绑定自己为推广人')
+    return
+  }
+  formLoading.value = true
+  bindUser.value = await BrokerageUserApi.getBrokerageUser(formData.value.bindUserId)
+  if (!bindUser.value) {
+    message.warning('推广员不存在')
+  }
+  formLoading.value = false
+}
+</script>

+ 307 - 0
src/views/guide/trade/brokerage/user/index.vue

@@ -0,0 +1,307 @@
+<template>
+  <doc-alert title="【交易】分销返佣" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="85px"
+    >
+      <el-form-item label="推广员编号" prop="bindUserId">
+        <el-input
+          v-model="queryParams.bindUserId"
+          placeholder="请输入推广员编号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="推广资格" prop="brokerageEnabled">
+        <el-select
+          v-model="queryParams.brokerageEnabled"
+          class="!w-240px"
+          clearable
+          placeholder="请选择推广资格"
+        >
+          <el-option label="有" :value="true" />
+          <el-option label="无" :value="false" />
+        </el-select>
+      </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" 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="brokerageUserCount" width="80px" />
+      <el-table-column
+        label="推广订单数量"
+        align="center"
+        prop="brokerageOrderCount"
+        min-width="110px"
+      />
+      <el-table-column
+        label="推广订单金额"
+        align="center"
+        prop="brokerageOrderPrice"
+        min-width="110px"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column
+        label="已提现金额"
+        align="center"
+        prop="withdrawPrice"
+        min-width="100px"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column label="已提现次数" align="center" prop="withdrawCount" min-width="100px" />
+      <el-table-column
+        label="未提现金额"
+        align="center"
+        prop="price"
+        min-width="100px"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column
+        label="冻结中佣金"
+        align="center"
+        prop="frozenPrice"
+        min-width="100px"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column label="推广资格" align="center" prop="brokerageEnabled" min-width="80px">
+        <template #default="scope">
+          <el-switch
+            v-model="scope.row.brokerageEnabled"
+            active-text="有"
+            inactive-text="无"
+            inline-prompt
+            :disabled="!checkPermi(['trade:brokerage-user:update-bind-user'])"
+            @change="handleBrokerageEnabledChange(scope.row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="成为推广员时间"
+        align="center"
+        prop="brokerageTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="上级推广员编号" align="center" prop="bindUserId" width="150px" />
+      <el-table-column
+        label="推广员绑定时间"
+        align="center"
+        prop="bindUserTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" width="150px" fixed="right">
+        <template #default="scope">
+          <el-dropdown
+            @command="(command) => handleCommand(command, scope.row)"
+            v-hasPermi="[
+              'trade:brokerage-user:user-query',
+              'trade:brokerage-user:order-query',
+              'trade:brokerage-user:update-bind-user',
+              'trade:brokerage-user:clear-bind-user'
+            ]"
+          >
+            <el-button link type="primary">
+              <Icon icon="ep:d-arrow-right" />
+              更多
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item
+                  command="openBrokerageUserTable"
+                  v-if="checkPermi(['trade:brokerage-user:user-query'])"
+                >
+                  推广人
+                </el-dropdown-item>
+                <el-dropdown-item
+                  command="openBrokerageOrderTable"
+                  v-if="checkPermi(['trade:brokerage-user:order-query'])"
+                >
+                  推广订单
+                </el-dropdown-item>
+                <el-dropdown-item
+                  command="openUpdateBindUserForm"
+                  v-if="checkPermi(['trade:brokerage-user:update-bind-user'])"
+                >
+                  修改上级推广人
+                </el-dropdown-item>
+                <el-dropdown-item
+                  command="handleClearBindUser"
+                  v-if="
+                    scope.row.bindUserId && checkPermi(['trade:brokerage-user:clear-bind-user'])
+                  "
+                >
+                  清除上级推广人
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+  <!-- 修改上级推广人表单 -->
+  <UpdateBindUserForm ref="updateBindUserFormRef" @success="getList" />
+  <!-- 推广人列表 -->
+  <BrokerageUserListDialog ref="brokerageUserListDialogRef" />
+  <!-- 推广订单列表 -->
+  <BrokerageOrderListDialog ref="brokerageOrderListDialogRef" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+import { checkPermi } from '@/utils/permission'
+import { fenToYuanFormat } from '@/utils/formatter'
+import UpdateBindUserForm from '@/views/mall/trade/brokerage/user/UpdateBindUserForm.vue'
+import BrokerageUserListDialog from '@/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue'
+import BrokerageOrderListDialog from '@/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue'
+
+defineOptions({ name: 'TradeBrokerageUser' })
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  bindUserId: null,
+  brokerageEnabled: true,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    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()
+}
+
+const handleCommand = (command: string, row: BrokerageUserApi.BrokerageUserVO) => {
+  switch (command) {
+    case 'openBrokerageUserTable':
+      openBrokerageUserTable(row.id)
+      break
+    case 'openBrokerageOrderTable':
+      openBrokerageOrderTable(row.id)
+      break
+    case 'openUpdateBindUserForm':
+      openUpdateBindUserForm(row)
+      break
+    case 'handleClearBindUser':
+      handleClearBindUser(row)
+      break
+  }
+}
+
+/** 打开推广人列表 */
+const brokerageUserListDialogRef = ref()
+const openBrokerageUserTable = (id: number) => {
+  brokerageUserListDialogRef.value.open(id)
+}
+
+/** 打开推广订单列表 */
+const brokerageOrderListDialogRef = ref()
+const openBrokerageOrderTable = (id: number) => {
+  brokerageOrderListDialogRef.value.open(id)
+}
+
+/** 打开表单:修改上级推广人 */
+const updateBindUserFormRef = ref()
+const openUpdateBindUserForm = (row: BrokerageUserApi.BrokerageUserVO) => {
+  updateBindUserFormRef.value.open(row)
+}
+
+/** 清除上级推广人 */
+const handleClearBindUser = async (row: BrokerageUserApi.BrokerageUserVO) => {
+  try {
+    // 二次确认
+    await message.confirm(`确认要清除"${row.nickname}"的上级推广人吗?`)
+    // 发起修改
+    await BrokerageUserApi.clearBindUser({ id: row.id })
+    message.success('清除成功')
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 推广资格:开通/关闭 */
+const handleBrokerageEnabledChange = async (row: BrokerageUserApi.BrokerageUserVO) => {
+  try {
+    // 二次确认
+    const text = row.brokerageEnabled ? '开通' : '关闭'
+    await message.confirm(`确认要${text}"${row.nickname}"的推广资格吗?`)
+    // 发起修改
+    await BrokerageUserApi.updateBrokerageEnabled({ id: row.id, enabled: row.brokerageEnabled })
+    message.success(text + '成功')
+    // 刷新列表
+    await getList()
+  } catch {
+    // 异常时,需要重置回之前的值
+    row.brokerageEnabled = !row.brokerageEnabled
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 73 - 0
src/views/guide/trade/brokerage/withdraw/BrokerageWithdrawRejectForm.vue

@@ -0,0 +1,73 @@
+<template>
+  <Dialog title="审核" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="驳回原因" prop="auditReason">
+        <el-input v-model="formData.auditReason" type="textarea" 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 BrokerageWithdrawApi from '@/api/mall/trade/brokerage/withdraw'
+
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  auditReason: undefined
+})
+const formRules = reactive({
+  auditReason: [{ required: true, message: '驳回原因不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id: number) => {
+  dialogVisible.value = true
+  resetForm()
+  formData.value.id = id
+}
+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 BrokerageWithdrawApi.BrokerageWithdrawVO
+    await BrokerageWithdrawApi.rejectBrokerageWithdraw(data)
+    message.success('驳回成功')
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    auditReason: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 268 - 0
src/views/guide/trade/brokerage/withdraw/index.vue

@@ -0,0 +1,268 @@
+<template>
+  <doc-alert title="【交易】分销返佣" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="用户编号" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          placeholder="请输入用户编号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="提现类型" prop="type">
+        <el-select
+          v-model="queryParams.type"
+          placeholder="请选择提现类型"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="账号" prop="accountNo">
+        <el-input
+          v-model="queryParams.accountNo"
+          placeholder="请输入账号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="提现银行" prop="bankName">
+        <el-select
+          v-model="queryParams.bankName"
+          placeholder="请选择提现银行"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.BROKERAGE_BANK_NAME)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_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"
+          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="left" prop="id" min-width="60px" />
+      <el-table-column label="用户信息" align="left" min-width="120px">
+        <template #default="scope">
+          <div>编号:{{ scope.row.userId }}</div>
+          <div>昵称:{{ scope.row.userNickname }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="提现金额" align="left" prop="price" min-width="80px">
+        <template #default="scope">
+          <div>金 额:¥{{ fenToYuan(scope.row.price) }}</div>
+          <div>手续费:¥{{ fenToYuan(scope.row.feePrice) }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="提现方式" align="left" prop="type" min-width="120px">
+        <template #default="scope">
+          <div v-if="scope.row.type === BrokerageWithdrawTypeEnum.WALLET.type"> 余额 </div>
+          <div v-else>
+            {{ getDictLabel(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, scope.row.type) }}
+            <span v-if="scope.row.accountNo">账号:{{ scope.row.accountNo }}</span>
+          </div>
+          <template v-if="scope.row.type === BrokerageWithdrawTypeEnum.BANK.type">
+            <div>真实姓名:{{ scope.row.name }}</div>
+            <div>
+              银行名称:
+              <dict-tag :type="DICT_TYPE.BROKERAGE_BANK_NAME" :value="scope.row.bankName" />
+            </div>
+            <div>开户地址:{{ scope.row.bankAddress }}</div>
+          </template>
+        </template>
+      </el-table-column>
+      <el-table-column label="收款码" align="left" prop="accountQrCodeUrl" min-width="70px">
+        <template #default="scope">
+          <el-image
+            v-if="scope.row.accountQrCodeUrl"
+            :src="scope.row.accountQrCodeUrl"
+            class="h-40px w-40px"
+            :preview-src-list="[scope.row.accountQrCodeUrl]"
+            preview-teleported
+          />
+          <span v-else>无</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="申请时间"
+        align="left"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="备注" align="left" prop="remark" />
+      <el-table-column label="状态" align="left" prop="status" min-width="120px">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BROKERAGE_WITHDRAW_STATUS" :value="scope.row.status" />
+          <div v-if="scope.row.auditTime" class="text-xs">
+            时间:{{ formatDate(scope.row.auditTime) }}
+          </div>
+          <div v-if="scope.row.auditReason" class="text-xs">
+            原因:{{ scope.row.auditReason }}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="left" width="110px" fixed="right">
+        <template #default="scope">
+          <template v-if="scope.row.status === BrokerageWithdrawStatusEnum.AUDITING.status">
+            <el-button
+              link
+              type="primary"
+              @click="handleApprove(scope.row.id)"
+              v-hasPermi="['trade:brokerage-withdraw:audit']"
+            >
+              通过
+            </el-button>
+            <el-button
+              link
+              type="danger"
+              @click="openForm(scope.row.id)"
+              v-hasPermi="['trade:brokerage-withdraw:audit']"
+            >
+              驳回
+            </el-button>
+          </template>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <BrokerageWithdrawRejectForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getDictLabel, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import * as BrokerageWithdrawApi from '@/api/mall/trade/brokerage/withdraw'
+import BrokerageWithdrawRejectForm from './BrokerageWithdrawRejectForm.vue'
+import { BrokerageWithdrawStatusEnum, BrokerageWithdrawTypeEnum } from '@/utils/constants'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'BrokerageWithdraw' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  userId: null,
+  type: null,
+  name: null,
+  accountNo: null,
+  bankName: null,
+  status: null,
+  auditReason: null,
+  auditTime: [],
+  remark: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BrokerageWithdrawApi.getBrokerageWithdrawPage(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 = (id: number) => {
+  formRef.value.open(id)
+}
+
+/** 审核通过 */
+const handleApprove = async (id: number) => {
+  try {
+    loading.value = true
+    await message.confirm('确定要审核通过吗?')
+    await BrokerageWithdrawApi.approveBrokerageWithdraw(id)
+    await message.success(t('common.success'))
+    await getList()
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 291 - 0
src/views/guide/trade/config/index.vue

@@ -0,0 +1,291 @@
+<template>
+  <doc-alert title="【交易】交易订单" url="https://doc.iocoder.cn/mall/trade-order/" />
+  <doc-alert title="【交易】购物车" url="https://doc.iocoder.cn/mall/trade-cart/" />
+
+  <ContentWrap>
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+    >
+      <el-form-item v-show="false" label="hideId">
+        <el-input v-model="formData.id" />
+      </el-form-item>
+      <el-tabs>
+        <!-- 售后 -->
+        <el-tab-pane label="售后">
+          <el-form-item label="退款理由" prop="afterSaleRefundReasons">
+            <el-select
+              v-model="formData.afterSaleRefundReasons"
+              allow-create
+              filterable
+              multiple
+              placeholder="请直接输入退款理由"
+            >
+              <el-option
+                v-for="reason in formData.afterSaleRefundReasons"
+                :key="reason"
+                :label="reason"
+                :value="reason"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="退货理由" prop="afterSaleReturnReasons">
+            <el-select
+              v-model="formData.afterSaleReturnReasons"
+              allow-create
+              filterable
+              multiple
+              placeholder="请直接输入退货理由"
+            >
+              <el-option
+                v-for="reason in formData.afterSaleReturnReasons"
+                :key="reason"
+                :label="reason"
+                :value="reason"
+              />
+            </el-select>
+          </el-form-item>
+        </el-tab-pane>
+        <!-- 配送 -->
+        <el-tab-pane label="配送">
+          <el-form-item label="启用包邮" prop="deliveryExpressFreeEnabled">
+            <el-switch v-model="formData.deliveryExpressFreeEnabled" style="user-select: none" />
+            <el-text class="w-full" size="small" type="info"> 商城是否启用全场包邮</el-text>
+          </el-form-item>
+          <el-form-item label="满额包邮" prop="deliveryExpressFreePrice">
+            <el-input-number
+              v-model="formData.deliveryExpressFreePrice"
+              :min="0"
+              :precision="2"
+              class="!w-xs"
+              placeholder="请输入满额包邮"
+            />
+            <el-text class="w-full" size="small" type="info">
+              商城商品满多少金额即可包邮,单位:元
+            </el-text>
+          </el-form-item>
+          <el-form-item label="启用门店自提" prop="deliveryPickUpEnabled">
+            <el-switch v-model="formData.deliveryPickUpEnabled" style="user-select: none" />
+          </el-form-item>
+        </el-tab-pane>
+        <!-- 分销 -->
+        <el-tab-pane label="分销">
+          <el-form-item label="分佣启用" prop="brokerageEnabled">
+            <el-switch v-model="formData.brokerageEnabled" style="user-select: none" />
+            <el-text class="w-full" size="small" type="info"> 商城是否开启分销模式</el-text>
+          </el-form-item>
+          <el-form-item label="分佣模式" prop="brokerageEnabledCondition">
+            <el-radio-group v-model="formData.brokerageEnabledCondition">
+              <el-radio
+                v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_ENABLED_CONDITION)"
+                :key="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-radio>
+            </el-radio-group>
+            <el-text class="w-full" size="small" type="info">
+              人人分销:每个用户都可以成为推广员
+            </el-text>
+            <el-text class="w-full" size="small" type="info">
+              指定分销:仅可在后台手动设置推广员
+            </el-text>
+          </el-form-item>
+          <el-form-item label="分销关系绑定" prop="brokerageBindMode">
+            <el-radio-group v-model="formData.brokerageBindMode">
+              <el-radio
+                v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_BIND_MODE)"
+                :key="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-radio>
+            </el-radio-group>
+            <el-text class="w-full" size="small" type="info">
+              首次绑定:只要用户没有推广人,随时都可以绑定推广关系
+            </el-text>
+            <el-text class="w-full" size="small" type="info">
+              注册绑定:只有新用户注册时或首次进入系统时才可以绑定推广关系
+            </el-text>
+          </el-form-item>
+          <el-form-item label="分销海报图">
+            <UploadImgs v-model="formData.brokeragePosterUrls" height="125px" width="75px" />
+            <el-text class="w-full" size="small" type="info">
+              个人中心分销海报图片,建议尺寸 600x1000
+            </el-text>
+          </el-form-item>
+          <el-form-item label="一级返佣比例" prop="brokerageFirstPercent">
+            <el-input-number
+              v-model="formData.brokerageFirstPercent"
+              :max="100"
+              :min="0"
+              class="!w-xs"
+              placeholder="请输入一级返佣比例"
+            />
+            <el-text class="w-full" size="small" type="info">
+              订单交易成功后给推广人返佣的百分比
+            </el-text>
+          </el-form-item>
+          <el-form-item label="二级返佣比例" prop="brokerageSecondPercent">
+            <el-input-number
+              v-model="formData.brokerageSecondPercent"
+              :max="100"
+              :min="0"
+              class="!w-xs"
+              placeholder="请输入二级返佣比例"
+            />
+            <el-text class="w-full" size="small" type="info">
+              订单交易成功后给推广人的推荐人返佣的百分比
+            </el-text>
+          </el-form-item>
+          <el-form-item label="佣金冻结天数" prop="brokerageFrozenDays">
+            <el-input-number
+              v-model="formData.brokerageFrozenDays"
+              :min="0"
+              class="!w-xs"
+              placeholder="请输入佣金冻结天数"
+            />
+            <el-text class="w-full" size="small" type="info">
+              防止用户退款,佣金被提现了,所以需要设置佣金冻结时间,单位:天
+            </el-text>
+          </el-form-item>
+          <el-form-item label="提现最低金额" prop="brokerageWithdrawMinPrice">
+            <el-input-number
+              v-model="formData.brokerageWithdrawMinPrice"
+              :min="0"
+              :precision="2"
+              class="!w-xs"
+              placeholder="请输入提现最低金额"
+            />
+            <el-text class="w-full" size="small" type="info">
+              用户提现最低金额限制,单位:元
+            </el-text>
+          </el-form-item>
+          <el-form-item label="提现手续费" prop="brokerageWithdrawFeePercent">
+            <el-input-number
+              v-model="formData.brokerageWithdrawFeePercent"
+              :max="100"
+              :min="0"
+              class="!w-xs"
+              placeholder="请输入提现手续费"
+            />
+            <el-text class="w-full" size="small" type="info">
+              提现手续费百分比,范围 0-100,0 为无提现手续费。例:设置 10,即收取 10% 手续费,提现
+              10 元,到账 9 元,1 元手续费
+            </el-text>
+          </el-form-item>
+          <el-form-item label="提现方式" prop="brokerageWithdrawTypes">
+            <el-checkbox-group v-model="formData.brokerageWithdrawTypes">
+              <el-checkbox
+                v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE)"
+                :key="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-checkbox>
+            </el-checkbox-group>
+            <el-text class="w-full" size="small" type="info"> 商城开通提现的付款方式</el-text>
+          </el-form-item>
+        </el-tab-pane>
+      </el-tabs>
+      <!-- 保存 -->
+      <el-form-item>
+        <el-button :loading="formLoading" type="primary" @click="submitForm"> 保存</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import * as ConfigApi from '@/api/mall/trade/config'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'TradeConfig' })
+
+const message = useMessage() // 消息弹窗
+
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formRef = ref()
+const formData = ref({
+  id: null,
+  afterSaleRefundReasons: [],
+  afterSaleReturnReasons: [],
+  deliveryExpressFreeEnabled: false,
+  deliveryExpressFreePrice: 0,
+  deliveryPickUpEnabled: false,
+  brokerageEnabled: false,
+  brokerageEnabledCondition: undefined,
+  brokerageBindMode: undefined,
+  brokeragePosterUrls: [],
+  brokerageFirstPercent: 0,
+  brokerageSecondPercent: 0,
+  brokerageWithdrawMinPrice: 0,
+  brokerageWithdrawFeePercent: 0,
+  brokerageFrozenDays: 0,
+  brokerageWithdrawTypes: []
+})
+const formRules = reactive({
+  deliveryExpressFreePrice: [{ required: true, message: '满额包邮不能为空', trigger: 'blur' }],
+  brokerageEnabledCondition: [{ required: true, message: '分佣模式不能为空', trigger: 'blur' }],
+  brokerageBindMode: [{ required: true, message: '分销关系绑定模式不能为空', trigger: 'blur' }],
+  brokerageFirstPercent: [{ required: true, message: '一级返佣比例不能为空', trigger: 'blur' }],
+  brokerageSecondPercent: [{ required: true, message: '二级返佣比例不能为空', trigger: 'blur' }],
+  brokerageWithdrawMinPrice: [
+    { required: true, message: '用户提现最低金额不能为空', trigger: 'blur' }
+  ],
+  brokerageWithdrawFeePercent: [{ required: true, message: '提现手续费不能为空', trigger: 'blur' }],
+  brokerageFrozenDays: [{ required: true, message: '佣金冻结时间不能为空', trigger: 'blur' }],
+  brokerageWithdrawTypes: [
+    {
+      required: true,
+      message: '提现方式不能为空',
+      trigger: 'change'
+    }
+  ]
+})
+
+const submitForm = async () => {
+  if (formLoading.value) return
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = cloneDeep(unref(formData.value)) as unknown as ConfigApi.ConfigVO
+    // 金额放大
+    data.deliveryExpressFreePrice = data.deliveryExpressFreePrice * 100
+    data.brokerageWithdrawMinPrice = data.brokerageWithdrawMinPrice * 100
+    await ConfigApi.saveTradeConfig(data)
+    message.success('保存成功')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 查询交易中心配置 */
+const getConfig = async () => {
+  formLoading.value = true
+  try {
+    const data = await ConfigApi.getTradeConfig()
+    if (data != null) {
+      formData.value = data
+      // 金额缩小
+      formData.value.deliveryExpressFreePrice = data.deliveryExpressFreePrice / 100
+      formData.value.brokerageWithdrawMinPrice = data.brokerageWithdrawMinPrice / 100
+    }
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getConfig()
+})
+</script>

+ 126 - 0
src/views/guide/trade/delivery/express/ExpressForm.vue

@@ -0,0 +1,126 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="公司编码" prop="code">
+        <el-input v-model="formData.code" placeholder="请输入快递编码" />
+      </el-form-item>
+      <el-form-item label="公司名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入快递名称" />
+      </el-form-item>
+      <el-form-item label="公司 logo" prop="logo">
+        <UploadImg v-model="formData.logo" :limit="1" :is-show-tip="false" />
+        <div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</div>
+      </el-form-item>
+      <el-form-item label="排序" prop="sort">
+        <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
+      </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>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+
+defineOptions({ name: 'ExpressForm' })
+
+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,
+  code: '',
+  name: '',
+  logo: '',
+  sort: 0,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  code: [{ required: true, message: '快递编码不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '分类名称不能为空', trigger: 'blur' }],
+  logo: [{ required: true, message: '分类图片不能为空', trigger: 'blur' }],
+  sort: [{ 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 DeliveryExpressApi.getDeliveryExpress(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 DeliveryExpressApi.DeliveryExpressVO
+    if (formType.value === 'create') {
+      await DeliveryExpressApi.createDeliveryExpress(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DeliveryExpressApi.updateDeliveryExpress(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: '',
+    picUrl: '',
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 189 - 0
src/views/guide/trade/delivery/express/index.vue

@@ -0,0 +1,189 @@
+<template>
+  <doc-alert title="【交易】快递发货" url="https://doc.iocoder.cn/mall/trade-delivery-express/" />
+
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="快递公司编号" prop="code">
+        <el-input
+          v-model="queryParams.code"
+          placeholder="请输快递公司编号"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="快递公司名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输快递公司名称"
+          clearable
+          @keyup.enter="handleQuery"
+          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-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['trade:delivery:express:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['trade:delivery:express:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="公司编码" prop="code" />
+      <el-table-column label="公司名称" prop="name" />
+      <el-table-column label="公司 logo " prop="logo">
+        <template #default="scope">
+          <img v-if="scope.row.logo" :src="scope.row.logo" alt="公司logo" class="h-40px" />
+        </template>
+      </el-table-column>
+      <el-table-column label="排序" align="center" prop="sort" />
+      <el-table-column label="开启状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['trade:delivery:express:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['trade:delivery:express:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ExpressForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import ExpressForm from './ExpressForm.vue'
+
+defineOptions({ name: 'Express' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const total = ref(0) // 列表的总页数
+const loading = ref(true) // 列表的加载中
+const list = ref<any[]>([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  code: '',
+  name: ''
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await DeliveryExpressApi.getDeliveryExpressPage(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 handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await DeliveryExpressApi.deleteDeliveryExpress(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await DeliveryExpressApi.exportDeliveryExpressApi(queryParams)
+    download.excel(data, '快递公司.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 321 - 0
src/views/guide/trade/delivery/expressTemplate/ExpressTemplateForm.vue

@@ -0,0 +1,321 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1300px">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="模板名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入模板名称" />
+      </el-form-item>
+      <el-form-item label="计费方式" prop="chargeMode">
+        <el-radio-group v-model="formData.chargeMode" @change="changeChargeMode">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="运费" prop="charges">
+        <el-table border style="width: 100%" :data="formData.charges">
+          <el-table-column align="center" label="区域" width="360">
+            <template #default="{ row }">
+              <el-cascader
+                v-model="row.areaIds"
+                :options="areaTree"
+                :props="defaultProps2"
+                class="w-1/1"
+                clearable
+                placeholder="请选择地区"
+                filterable
+                collapse-tags
+              />
+            </template>
+          </el-table-column>
+          <el-table-column
+            align="center"
+            :label="columnTitle.startCountTitle"
+            width="180"
+            prop="startCount"
+          >
+            <template #default="{ row }">
+              <el-input-number v-model="row.startCount" :min="1" />
+            </template>
+          </el-table-column>
+          <el-table-column width="180" align="center" label="运费(元)" prop="startPrice">
+            <template #default="{ row }">
+              <el-input-number v-model="row.startPrice" :min="1" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            width="180"
+            align="center"
+            :label="columnTitle.extraCountTitle"
+            prop="extraCount"
+          >
+            <template #default="{ row }">
+              <el-input-number v-model="row.extraCount" :min="1" />
+            </template>
+          </el-table-column>
+          <el-table-column width="180" align="center" label="续费(元)" prop="extraPrice">
+            <template #default="{ row }">
+              <el-input-number v-model="row.extraPrice" :min="1" />
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center">
+            <template #default="scope">
+              <el-button link type="danger" @click="deleteChargeArea(scope.$index)">
+                删除
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" plain @click="addChargeArea()">
+          <Icon icon="ep:plus" class="mr-5px" /> 添加区域
+        </el-button>
+      </el-form-item>
+      <el-form-item label="包邮区域" prop="frees">
+        <el-table border style="width: 100%" :data="formData.frees">
+          <el-table-column align="center" label="区域" width="360">
+            <template #default="{ row }">
+              <el-cascader
+                v-model="row.areaIds"
+                :options="areaTree"
+                :props="defaultProps2"
+                class="w-1/1"
+                clearable
+                placeholder="请选择商品分类"
+                filterable
+                collapse-tags
+              />
+            </template>
+          </el-table-column>
+          <el-table-column align="center" :label="columnTitle.freeCountTitle" prop="freeCount">
+            <template #default="{ row }">
+              <el-input-number v-model="row.freeCount" :min="1" />
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="包邮金额(元)" prop="freePrice">
+            <template #default="{ row }">
+              <el-input-number v-model="row.freePrice" :min="1" />
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center">
+            <template #default="scope">
+              <el-button link type="danger" @click="deleteFreeArea(scope.$index)"> 删除 </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" plain @click="addFreeArea()">
+          <Icon icon="ep:plus" class="mr-5px" /> 添加区域
+        </el-button>
+      </el-form-item>
+      <el-form-item label="排序" prop="sort">
+        <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
+      </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 lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as DeliveryExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
+import * as AreaApi from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+import { yuanToFen, fenToYuan } from '@/utils'
+import { cloneDeep } from 'lodash-es'
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const defaultProps2 = {
+  ...defaultProps,
+  multiple: true
+}
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: '',
+  chargeMode: 1,
+  sort: 0,
+  charges: [],
+  frees: []
+})
+const columnTitleMap = new Map()
+const columnTitle = ref({
+  startCountTitle: '首件',
+  extraCountTitle: '续件',
+  freeCountTitle: '包邮件数'
+})
+const formRules = reactive({
+  name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }],
+  chargeMode: [{ required: true, message: '配送计费方式不能为空', trigger: 'blur' }],
+  sort: [{ 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()
+  try {
+    // 修改时,设置数据
+    if (id) {
+      formLoading.value = true
+      formData.value = await DeliveryExpressTemplateApi.getDeliveryExpressTemplate(id)
+      columnTitle.value = columnTitleMap.get(formData.value.chargeMode)
+      formData.value.charges.forEach((item) => {
+        // 前端价格以元展示
+        item.startPrice = fenToYuan(item.startPrice)
+        item.extraPrice = fenToYuan(item.extraPrice)
+      })
+      formData.value.frees.forEach((item) => {
+        item.freePrice = fenToYuan(item.freePrice)
+      })
+    }
+  } 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 = cloneDeep(formData.value) as DeliveryExpressTemplateApi.DeliveryExpressTemplateVO
+    // 前端价格以元展示,提交到后端。用分计算
+    data.charges.forEach((item) => {
+      item.startPrice = yuanToFen(item.startPrice)
+      item.extraPrice = yuanToFen(item.extraPrice)
+    })
+    data.frees.forEach((item) => {
+      item.freePrice = yuanToFen(item.freePrice)
+    })
+    if (formType.value === 'create') {
+      await DeliveryExpressTemplateApi.createDeliveryExpressTemplate(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DeliveryExpressTemplateApi.updateDeliveryExpressTemplate(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: '',
+    chargeMode: 1,
+    charges: [
+      {
+        areaIds: [1],
+        startCount: 2,
+        startPrice: 5,
+        extraCount: 5,
+        extraPrice: 10
+      }
+    ],
+    frees: [],
+    sort: 0
+  }
+  columnTitle.value = columnTitleMap.get(1)
+  formRef.value?.resetFields()
+}
+
+/** 配送计费方法改变 */
+const changeChargeMode = (chargeMode: number) => {
+  columnTitle.value = columnTitleMap.get(chargeMode)
+}
+
+/** 初始化数据 */
+const areaTree = ref([])
+const initData = async () => {
+  // 表头标题和计费方式的映射
+  columnTitleMap.set(1, {
+    startCountTitle: '首件',
+    extraCountTitle: '续件',
+    freeCountTitle: '包邮件数'
+  })
+  columnTitleMap.set(2, {
+    startCountTitle: '首件重量(kg)',
+    extraCountTitle: '续件重量(kg)',
+    freeCountTitle: '包邮重量(kg)'
+  })
+  columnTitleMap.set(3, {
+    startCountTitle: '首件体积(m³)',
+    extraCountTitle: '续件体积(m³)',
+    freeCountTitle: '包邮体积(m³)'
+  })
+  // 加载区域数据
+  areaTree.value = await AreaApi.getAreaTree()
+}
+
+/** 添加计费区域 */
+const addChargeArea = () => {
+  const data = formData.value
+  data.charges.push({
+    areaIds: [],
+    startCount: 1,
+    startPrice: 1,
+    extraCount: 1,
+    extraPrice: 1
+  })
+}
+
+/** 删除计费区域 */
+const deleteChargeArea = (index) => {
+  const data = formData.value
+  data.charges.splice(index, 1)
+}
+
+/** 添加包邮区域 */
+const addFreeArea = () => {
+  const data = formData.value
+  data.frees.push({
+    areaIds: [],
+    freeCount: 1,
+    freePrice: 1
+  })
+}
+
+/** 删除包邮区域 */
+const deleteFreeArea = (index) => {
+  const data = formData.value
+  data.frees.splice(index, 1)
+}
+
+/** 初始化 **/
+onMounted(() => {
+  initData()
+})
+</script>

+ 165 - 0
src/views/guide/trade/delivery/expressTemplate/index.vue

@@ -0,0 +1,165 @@
+<template>
+  <doc-alert title="【交易】快递发货" url="https://doc.iocoder.cn/mall/trade-delivery-express/" />
+
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="100px"
+    >
+      <el-form-item label="模板名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入模板名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="计费方式" prop="chargeMode">
+        <el-select
+          v-model="queryParams.chargeMode"
+          placeholder="计费方式"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </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-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['trade:delivery:express-template:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />
+          新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="编号" min-width="60" prop="id" />
+      <el-table-column label="模板名称" min-width="100" prop="name" />
+      <el-table-column label="计费方式" prop="chargeMode" min-width="100" align="center">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.EXPRESS_CHARGE_MODE" :value="scope.row.chargeMode" />
+        </template>
+      </el-table-column>
+      <el-table-column label="排序" min-width="100" prop="sort" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        width="180"
+        :formatter="dateFormatter"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['trade:delivery:express-template:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['trade:delivery:express-template:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <ExpressTemplateForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as DeliveryExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
+import ExpressTemplateForm from './ExpressTemplateForm.vue'
+
+defineOptions({ name: 'DeliveryExpressTemplate' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+const total = ref(0) // 列表的总页数
+const loading = ref(true) // 列表的加载中
+const list = ref<any[]>([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: '',
+  chargeMode: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await DeliveryExpressTemplateApi.getDeliveryExpressTemplatePage(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 handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await DeliveryExpressTemplateApi.deleteDeliveryExpressTemplate(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 328 - 0
src/views/guide/trade/delivery/pickUpOrder/index.vue

@@ -0,0 +1,328 @@
+<template>
+  <doc-alert title="【交易】交易订单" url="https://doc.iocoder.cn/mall/trade-order/" />
+  <doc-alert title="【交易】购物车" url="https://doc.iocoder.cn/mall/trade-cart/" />
+
+  <!-- 搜索 -->
+  <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"
+          :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="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 label="聚合搜索">
+        <el-input
+          v-show="true"
+          v-model="queryParams[queryType.queryParam]"
+          class="!w-280px"
+          clearable
+          placeholder="请输入"
+          :type="queryType.queryParam === 'userId' ? 'number' : 'text'"
+        >
+          <template #prepend>
+            <el-select
+              v-model="queryType.queryParam"
+              class="!w-110px"
+              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-button @click="handlePickup" type="success" plain v-hasPermi="['trade:order:pick-up']">
+          <Icon class="mr-5px" icon="ep:check" />
+          核销
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 统计卡片 -->
+  <el-row :gutter="16" class="summary">
+    <el-col :sm="6" :xs="12" v-loading="loading">
+      <SummaryCard
+        title="订单数量"
+        icon="icon-park-outline:transaction-order"
+        icon-color="bg-blue-100"
+        icon-bg-color="text-blue-500"
+        :value="summary?.orderCount || 0"
+      />
+    </el-col>
+    <el-col :sm="6" :xs="12" v-loading="loading">
+      <SummaryCard
+        title="订单金额"
+        icon="streamline:money-cash-file-dollar-common-money-currency-cash-file"
+        icon-color="bg-purple-100"
+        icon-bg-color="text-purple-500"
+        prefix="¥"
+        :decimals="2"
+        :value="fenToYuan(summary?.orderPayPrice || 0)"
+      />
+    </el-col>
+    <el-col :sm="6" :xs="12" v-loading="loading">
+      <SummaryCard
+        title="退款单数"
+        icon="heroicons:receipt-refund"
+        icon-color="bg-yellow-100"
+        icon-bg-color="text-yellow-500"
+        :value="summary?.afterSaleCount || 0"
+      />
+    </el-col>
+    <el-col :sm="6" :xs="12" v-loading="loading">
+      <SummaryCard
+        title="退款金额"
+        icon="ri:refund-2-line"
+        icon-color="bg-green-100"
+        icon-bg-color="text-green-500"
+        prefix="¥"
+        :decimals="2"
+        :value="fenToYuan(summary?.afterSalePrice || 0)"
+      />
+    </el-col>
+  </el-row>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="订单号" align="center" prop="no" min-width="180" />
+      <el-table-column label="用户信息" align="center" prop="user.nickname" min-width="80" />
+      <el-table-column
+        label="推荐人信息"
+        align="center"
+        prop="brokerageUser.nickname"
+        min-width="100"
+      />
+      <el-table-column label="商品信息" align="center" prop="spuName" min-width="300">
+        <template #default="{ row }">
+          <div class="flex items-center" v-for="item in row.items" :key="item.id">
+            <el-image
+              :src="item.picUrl"
+              class="mr-10px h-30px w-30px flex-shrink-0"
+              :preview-src-list="[item.picUrl]"
+              preview-teleported
+            />
+            <span class="mr-10px">{{ item.spuName }}</span>
+            <div class="flex flex-col flex-wrap gap-1">
+              <el-tag
+                v-for="property in item.properties"
+                :key="property.propertyId"
+                class="mr-10px"
+              >
+                {{ property.propertyName }}: {{ property.valueName }}
+              </el-tag>
+              <span>{{ floatToFixed2(item.price) }} 元 x {{ item.count }}</span>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="实付金额(元)"
+        align="center"
+        prop="payPrice"
+        min-width="110"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column label="核销员" align="center" prop="storeStaffName" min-width="70" />
+      <el-table-column label="核销门店" align="center" prop="pickUpStoreId" min-width="80">
+        <template #default="{ row }">
+          {{ pickUpStoreList.find((p) => p.id === row.pickUpStoreId)?.name }}
+        </template>
+      </el-table-column>
+      <el-table-column label="支付状态" align="center" prop="payStatus" min-width="80">
+        <template #default="{ row }">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.payStatus || false" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="订单状态" prop="status" width="120">
+        <template #default="{ row }">
+          <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="下单时间"
+        align="center"
+        prop="createTime"
+        min-width="170"
+        :formatter="dateFormatter"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 各种操作的弹窗 -->
+  <OrderPickUpForm ref="pickUpForm" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import type { FormInstance } from 'element-plus'
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import { DICT_TYPE } from '@/utils/dict'
+import { fenToYuan, floatToFixed2 } from '@/utils'
+import { fenToYuanFormat } from '@/utils/formatter'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { dateFormatter } from '@/utils/formatTime'
+import { DeliveryTypeEnum } from '@/utils/constants'
+import { TradeOrderSummaryRespVO } from '@/api/mall/trade/order'
+import { DeliveryPickUpStoreVO } from '@/api/mall/trade/delivery/pickUpStore'
+import OrderPickUpForm from '@/views/mall/trade/order/form/OrderPickUpForm.vue'
+
+defineOptions({ name: 'PickUpOrder' })
+
+// 列表的加载中
+const loading = ref(true)
+// 列表的总页数
+const total = ref(2)
+// 列表的数据
+const list = ref<TradeOrderApi.OrderVO[]>([])
+// 搜索的表单
+const queryFormRef = ref<FormInstance>()
+// 初始表单参数
+const INIT_QUERY_PARAMS = {
+  // 页数
+  pageNo: 1,
+  // 每页显示数量
+  pageSize: 10,
+  // 创建时间
+  createTime: undefined,
+  // 配送方式
+  deliveryType: DeliveryTypeEnum.PICK_UP.type,
+  // 自提门店
+  pickUpStoreId: undefined
+}
+// 表单搜索
+const queryParams = ref({ ...INIT_QUERY_PARAMS })
+// 订单搜索类型 queryParam
+const queryType = reactive({ queryParam: 'no' })
+// 订单统计数据
+const summary = ref<TradeOrderSummaryRespVO>()
+
+// 订单聚合搜索 select 类型配置(动态搜索)
+const dynamicSearchList = ref([
+  { value: 'no', label: '订单号' },
+  { value: 'userId', label: '用户UID' },
+  { value: 'userNickname', label: '用户昵称' },
+  { value: 'userMobile', label: '用户电话' }
+])
+/**
+ * 聚合搜索切换查询对象时触发
+ * @param val
+ */
+const inputChangeSelect = (val: string) => {
+  dynamicSearchList.value
+    .filter((item) => item.value !== val)
+    ?.forEach((item) => {
+      // 清除集合搜索无用属性
+      if (queryParams.value.hasOwnProperty(item.value)) {
+        delete queryParams.value[item.value]
+      }
+    })
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // 统计
+    summary.value = await TradeOrderApi.getOrderSummary(unref(queryParams))
+    // 分页
+    const data = await TradeOrderApi.getOrderPage(unref(queryParams))
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  queryParams.value.pageNo = 1
+  await getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.value = { ...INIT_QUERY_PARAMS }
+  handleQuery()
+}
+
+/** 自提门店精简列表 */
+const pickUpStoreList = ref<DeliveryPickUpStoreVO[]>([])
+const getPickUpStoreList = async () => {
+  pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+}
+
+/** 显示核销表单 */
+const pickUpForm = ref()
+const handlePickup = () => {
+  pickUpForm.value.open()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+  getPickUpStoreList()
+})
+</script>
+<style lang="scss" scoped>
+:deep(.order-table-col > .cell) {
+  padding: 0;
+}
+
+.summary {
+  .el-col {
+    margin-bottom: 1rem;
+  }
+}
+</style>

+ 273 - 0
src/views/guide/trade/delivery/pickUpStore/PickUpStoreForm.vue

@@ -0,0 +1,273 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="60%">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="门店 logo" prop="logo">
+            <UploadImg v-model="formData.logo" :limit="1" :is-show-tip="false" />
+            <div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</div>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <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-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="门店名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入门店名称" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="门店手机" prop="phone">
+            <el-input v-model="formData.phone" placeholder="请输入门店手机" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="门店简介" prop="introduction">
+        <el-input
+          v-model="formData.introduction"
+          :rows="3"
+          type="textarea"
+          placeholder="请输入门店简介"
+        />
+      </el-form-item>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="门店所在地区" prop="areaId">
+            <el-cascader v-model="formData.areaId" :options="areaList" :props="defaultProps" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="门店详细地址" prop="detailAddress">
+            <el-input v-model="formData.detailAddress" placeholder="请输入门店详细地址" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="营业开始时间" prop="openingTime">
+            <el-time-select
+              v-model="formData.openingTime"
+              :max-time="formData.closingTime"
+              placeholder="开始时间"
+              start="08:30"
+              step="00:15"
+              end="23:30"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="营业结束时间" prop="closingTime">
+            <el-time-select
+              v-model="formData.closingTime"
+              :min-time="formData.openingTime"
+              placeholder="结束时间"
+              start="08:30"
+              step="00:15"
+              end="23:30"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="经度" prop="longitude">
+            <el-input v-model="formData.longitude" placeholder="请输入门店经度" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="纬度" prop="latitude">
+            <el-input v-model="formData.latitude" placeholder="请输入门店纬度" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="获取经纬度">
+        <el-button type="primary" @click="mapDialogVisible = true">获取</el-button>
+      </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>
+    <el-dialog
+      v-model="mapDialogVisible"
+      title="获取经纬度"
+      append-to-body
+      width="500px"
+      class="mapBox"
+    >
+      <iframe id="mapPage" width="100%" height="100%" frameborder="0" :src="tencentLbsUrl"></iframe>
+    </el-dialog>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import { defaultProps } from '@/utils/tree'
+import { getAreaTree } from '@/api/system/area'
+import * as ConfigApi from '@/api/mall/trade/config'
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const mapDialogVisible = ref(false) // 地图弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: '',
+  phone: '',
+  logo: '',
+  detailAddress: '',
+  introduction: '',
+  areaId: 0,
+  openingTime: undefined,
+  closingTime: undefined,
+  latitude: undefined,
+  longitude: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  name: [{ required: true, message: '门店名称不能为空', trigger: 'blur' }],
+  logo: [{ required: true, message: '门店 logo 不能为空', trigger: 'blur' }],
+  phone: [
+    { required: true, message: '门店手机不能为空', trigger: 'blur' },
+    { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
+  ],
+  areaId: [{ required: true, message: '门店所在区域不能为空', trigger: 'blur' }],
+  detailAddress: [{ required: true, message: '门店详细地址不能为空', trigger: 'blur' }],
+  openingTime: [{ required: true, message: '营业开始时间不能为空', trigger: 'blur' }],
+  closingTime: [{ required: true, message: '营业结束时间不能为空', trigger: 'blur' }],
+  latitude: [{ required: true, message: '纬度不能为空', trigger: 'blur' }],
+  longitude: [{ required: true, message: '经度不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const areaList = ref() // 区域树
+const tencentLbsUrl = ref('') // 腾讯位置服务 url
+
+/** 打开弹窗 */
+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 DeliveryPickUpStoreApi.getDeliveryPickUpStore(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 DeliveryPickUpStoreApi.DeliveryPickUpStoreVO
+    if (formType.value === 'create') {
+      await DeliveryPickUpStoreApi.createDeliveryPickUpStore(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await DeliveryPickUpStoreApi.updateDeliveryPickUpStore(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: '',
+    phone: '',
+    logo: '',
+    detailAddress: '',
+    introduction: '',
+    areaId: undefined,
+    openingTime: undefined,
+    closingTime: undefined,
+    latitude: undefined,
+    longitude: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+
+/** 选择经纬度 */
+const selectAddress = function (loc: any): void {
+  if (loc.latlng && loc.latlng.lat) {
+    formData.value.latitude = loc.latlng.lat
+  }
+  if (loc.latlng && loc.latlng.lng) {
+    formData.value.longitude = loc.latlng.lng
+  }
+  mapDialogVisible.value = false
+}
+
+/** 初始化腾讯地图 */
+const initTencentLbsMap = async () => {
+  window.selectAddress = selectAddress
+  window.addEventListener(
+    'message',
+    function (event) {
+      // 接收位置信息,用户选择确认位置点后选点组件会触发该事件,回传用户的位置信息
+      let loc = event.data
+      if (loc && loc.module === 'locationPicker') {
+        // 防止其他应用也会向该页面 post 信息,需判断 module 是否为 'locationPicker'
+        window.parent.selectAddress(loc)
+      }
+    },
+    false
+  )
+  const data = await ConfigApi.getTradeConfig()
+  const key = data.tencentLbsKey
+  tencentLbsUrl.value = `https://apis.map.qq.com/tools/locpicker?type=1&key=${key}&referer=myapp`
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  areaList.value = await getAreaTree()
+  // 加载地图
+  await initTencentLbsMap()
+})
+</script>
+<style lang="scss">
+.mapBox .el-dialog__body {
+  height: 640px !important;
+}
+</style>

+ 190 - 0
src/views/guide/trade/delivery/pickUpStore/index.vue

@@ -0,0 +1,190 @@
+<template>
+  <doc-alert title="【交易】快递发货" url="https://doc.iocoder.cn/mall/trade-delivery-express/" />
+
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px">
+      <el-form-item label="门店手机" prop="phone">
+        <el-input
+          v-model="queryParams.phone"
+          class="!w-240px"
+          clearable
+          placeholder="请输门店手机"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="门店名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          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"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="datetimerange"
+          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="['trade:delivery:pick-up-store: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 label="编号" min-width="80" prop="id" />
+      <el-table-column label="门店 logo" min-width="100" prop="logo">
+        <template #default="scope">
+          <img v-if="scope.row.logo" :src="scope.row.logo" alt="门店 logo" class="h-50px" />
+        </template>
+      </el-table-column>
+      <el-table-column label="门店名称" min-width="150" prop="name" />
+      <el-table-column label="门店手机" min-width="100" prop="phone" />
+      <el-table-column label="地址" min-width="100" prop="detailAddress" />
+      <el-table-column label="营业时间" min-width="180">
+        <template #default="scope">
+          {{ scope.row.openingTime }} ~ {{ scope.row.closingTime }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="开启状态" min-width="100" 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="180"
+      />
+      <el-table-column align="center" label="操作">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['trade:delivery:pick-up-store:update']"
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+          >
+            编辑
+          </el-button>
+          <el-button
+            v-hasPermi="['trade:delivery:pick-up-store:delete']"
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+  <!-- 表单弹窗:添加/修改 -->
+  <DeliveryPickUpStoreForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" name="DeliveryPickUpStore" setup>
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import DeliveryPickUpStoreForm from './PickUpStoreForm.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const total = ref(0) // 列表的总页数
+const loading = ref(true) // 列表的加载中
+const list = ref<any[]>([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  status: undefined,
+  phone: undefined,
+  name: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await DeliveryPickUpStoreApi.deleteDeliveryPickUpStore(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await DeliveryPickUpStoreApi.getDeliveryPickUpStorePage(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>

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff