ydmyzx пре 5 месеци
родитељ
комит
4538762b4f

+ 68 - 0
src/api/mall/promotion/bargain/bargainActivity.ts

@@ -0,0 +1,68 @@
+import request from '@/config/axios'
+import { Sku, Spu } from '@/api/mall/product/spu'
+
+export interface BargainActivityVO {
+  id?: number
+  name?: string
+  startTime?: Date
+  endTime?: Date
+  status?: number
+  helpMaxCount?: number // 达到该人数,才能砍到低价
+  bargainCount?: number // 最大帮砍次数
+  totalLimitCount?: number // 最大购买次数
+  spuId: number
+  skuId: number
+  bargainFirstPrice: number // 砍价起始价格,单位分
+  bargainMinPrice: number // 砍价底价
+  stock: number // 活动库存
+  randomMinPrice?: number // 用户每次砍价的最小金额,单位:分
+  randomMaxPrice?: number // 用户每次砍价的最大金额,单位:分
+}
+
+// 砍价活动所需属性。选择的商品和属性的时候使用方便使用活动的通用封装
+export interface BargainProductVO {
+  spuId: number
+  skuId: number
+  bargainFirstPrice: number // 砍价起始价格,单位分
+  bargainMinPrice: number // 砍价底价
+  stock: number // 活动库存
+}
+
+// 扩展 Sku 配置
+export type SkuExtension = Sku & {
+  productConfig: BargainProductVO
+}
+
+export interface SpuExtension extends Spu {
+  skus: SkuExtension[] // 重写类型
+}
+
+// 查询砍价活动列表
+export const getBargainActivityPage = async (params: any) => {
+  return await request.get({ url: '/promotion/bargain-activity/page', params })
+}
+
+// 查询砍价活动详情
+export const getBargainActivity = async (id: number) => {
+  return await request.get({ url: '/promotion/bargain-activity/get?id=' + id })
+}
+
+// 新增砍价活动
+export const createBargainActivity = async (data: BargainActivityVO) => {
+  return await request.post({ url: '/promotion/bargain-activity/create', data })
+}
+
+// 修改砍价活动
+export const updateBargainActivity = async (data: BargainActivityVO) => {
+  return await request.put({ url: '/promotion/bargain-activity/update', data })
+}
+
+// 关闭砍价活动
+export const closeBargainActivity = async (id: number) => {
+  return await request.put({ url: '/promotion/bargain-activity/close?id=' + id })
+}
+
+// 删除砍价活动
+export const deleteBargainActivity = async (id: number) => {
+  return await request.delete({ url: '/promotion/bargain-activity/delete?id=' + id })
+}

BIN
src/assets/imgs/avatar.jpg


+ 17 - 0
src/components/Backtop/src/Backtop.vue

@@ -0,0 +1,17 @@
+<script lang="ts" setup>
+import { ElBacktop } from 'element-plus'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'BackTop' })
+
+const { getPrefixCls, variables } = useDesign()
+
+const prefixCls = getPrefixCls('backtop')
+</script>
+
+<template>
+  <ElBacktop
+    :class="`${prefixCls}-backtop`"
+    :target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
+  />
+</template>

+ 72 - 0
src/layout/components/AppView.vue

@@ -0,0 +1,72 @@
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useAppStore } from '@/store/modules/app'
+import { Footer } from '@/layout/components/Footer'
+
+defineOptions({ name: 'AppView' })
+
+const appStore = useAppStore()
+
+const layout = computed(() => appStore.getLayout)
+
+const fixedHeader = computed(() => appStore.getFixedHeader)
+
+const footer = computed(() => appStore.getFooter)
+
+const tagsViewStore = useTagsViewStore()
+
+const getCaches = computed((): string[] => {
+  return tagsViewStore.getCachedViews
+})
+
+const tagsView = computed(() => appStore.getTagsView)
+
+//region 无感刷新
+const routerAlive = ref(true)
+// 无感刷新,防止出现页面闪烁白屏
+const reload = () => {
+  routerAlive.value = false
+  nextTick(() => (routerAlive.value = true))
+}
+// 为组件后代提供刷新方法
+provide('reload', reload)
+//endregion
+</script>
+
+<template>
+  <section
+    :class="[
+      'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
+      {
+        '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
+          (fixedHeader &&
+            (layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
+            footer) ||
+          (!tagsView && layout === 'top' && footer),
+        '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
+          tagsView && layout === 'top' && footer,
+
+        '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
+          !fixedHeader && layout === 'classic' && footer,
+
+        '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
+          !fixedHeader && layout === 'topLeft' && footer,
+
+        '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
+          fixedHeader && layout === 'cutMenu' && footer,
+
+        '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
+          !fixedHeader && layout === 'cutMenu' && footer
+      }
+    ]"
+  >
+    <router-view v-if="routerAlive">
+      <template #default="{ Component, route }">
+        <keep-alive :include="getCaches">
+          <component :is="Component" :key="route.fullPath" />
+        </keep-alive>
+      </template>
+    </router-view>
+  </section>
+  <Footer v-if="footer" />
+</template>

+ 71 - 0
src/utils/auth.ts

@@ -0,0 +1,71 @@
+import { useCache, CACHE_KEY } from '@/hooks/web/useCache'
+import { TokenType } from '@/api/login/types'
+import { decrypt, encrypt } from '@/utils/jsencrypt'
+
+const { wsCache } = useCache()
+
+const AccessTokenKey = 'ACCESS_TOKEN'
+const RefreshTokenKey = 'REFRESH_TOKEN'
+
+// 获取token
+export const getAccessToken = () => {
+  // 此处与TokenKey相同,此写法解决初始化时Cookies中不存在TokenKey报错
+  return wsCache.get(AccessTokenKey) ? wsCache.get(AccessTokenKey) : wsCache.get('ACCESS_TOKEN')
+}
+
+// 刷新token
+export const getRefreshToken = () => {
+  return wsCache.get(RefreshTokenKey)
+}
+
+// 设置token
+export const setToken = (token: TokenType) => {
+  wsCache.set(RefreshTokenKey, token.refreshToken)
+  wsCache.set(AccessTokenKey, token.accessToken)
+}
+
+// 删除token
+export const removeToken = () => {
+  wsCache.delete(AccessTokenKey)
+  wsCache.delete(RefreshTokenKey)
+}
+
+/** 格式化token(jwt格式) */
+export const formatToken = (token: string): string => {
+  return 'Bearer ' + token
+}
+// ========== 账号相关 ==========
+
+export type LoginFormType = {
+  tenantName: string
+  username: string
+  password: string
+  rememberMe: boolean
+}
+
+export const getLoginForm = () => {
+  const loginForm: LoginFormType = wsCache.get(CACHE_KEY.LoginForm)
+  if (loginForm) {
+    loginForm.password = decrypt(loginForm.password) as string
+  }
+  return loginForm
+}
+
+export const setLoginForm = (loginForm: LoginFormType) => {
+  loginForm.password = encrypt(loginForm.password) as string
+  wsCache.set(CACHE_KEY.LoginForm, loginForm, { exp: 30 * 24 * 60 * 60 })
+}
+
+export const removeLoginForm = () => {
+  wsCache.delete(CACHE_KEY.LoginForm)
+}
+
+// ========== 租户相关 ==========
+
+export const getTenantId = () => {
+  return wsCache.get(CACHE_KEY.TenantId)
+}
+
+export const setTenantId = (username: string) => {
+  wsCache.set(CACHE_KEY.TenantId, username)
+}

+ 268 - 0
src/views/bpm/disbursement/AuthorityForm.vue

@@ -0,0 +1,268 @@
+<template>
+  <Dialog title="权限管理" v-model="dialogVisible" width="70%">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+<!--      <el-form-item label="角色">-->
+<!--        <el-select v-model="formData.roleIds" multiple placeholder="请选择角色">-->
+<!--          <el-option v-for="item in roleList" :key="item.id" :label="item.name" :value="item.id" />-->
+<!--        </el-select>-->
+<!--      </el-form-item>-->
+
+<!--      <el-form-item label="用户">-->
+<!--        <el-select v-model="formData.userIds" multiple placeholder="请选择用户">-->
+<!--          <el-option v-for="item in userOptions" :key="item.id" :label="item.nickname" :value="item.id" />-->
+<!--        </el-select>-->
+<!--      </el-form-item>-->
+
+<!--      <el-form-item label="只读角色">-->
+<!--        <el-select v-model="formData.readRoleIds" multiple placeholder="请选择角色">-->
+<!--          <el-option v-for="item in roleList" :key="item.id" :label="item.name" :value="item.id" />-->
+<!--        </el-select>-->
+<!--      </el-form-item>-->
+
+<!--      <el-form-item label="只读用户">-->
+<!--        <el-select v-model="formData.readUserIds" multiple placeholder="请选择用户">-->
+<!--          <el-option v-for="item in userOptions" :key="item.id" :label="item.nickname" :value="item.id" />-->
+<!--        </el-select>-->
+<!--      </el-form-item>-->
+
+    </el-form>
+    <ContentWrap>
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="100px"
+        >
+          <el-form-item label="昵称" prop="username">
+            <el-input
+              v-model="queryParams.nickname"
+              placeholder="请输入用户昵称"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="手机号码" prop="mobile">
+            <el-input
+              v-model="queryParams.mobile"
+              placeholder="请输入手机号码"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+
+          <el-form-item>
+            <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
+            <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
+
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+    <ContentWrap>
+        <el-table v-loading="loading" :data="list">
+          <el-table-column label="用户编号" align="center" key="id" prop="id" />
+          <el-table-column
+            label="用户昵称"
+            align="center"
+            prop="nickname"
+            :show-overflow-tooltip="true"
+          />
+          <el-table-column
+            label="部门"
+            align="center"
+            key="deptName"
+            prop="deptName"
+            :show-overflow-tooltip="true"
+          />
+          <el-table-column label="手机号码" align="center" prop="mobile" width="120" />
+          <el-table-column label="成员权限" key="status" min-width="200px">
+            <template #default="scope">
+              <el-select v-model="scope.row.onlyRead" laceholder="请选择成员权限"
+                @change="handlePermissionChange(scope.row)"
+                         :disabled="scope.row.isSelf"
+              >
+                <el-option label="管理权限" :value="1" />
+                <el-option label="操作权限" :value="2" />
+                <el-option label="只读权限" :value="3" />
+              </el-select>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center" width="160">
+            <template #default="scope">
+              <div class="flex items-center justify-center">
+                <el-button
+                  type="primary"
+                  link
+                  @click="deleteQuery(scope.row.id)"
+                  :disabled="scope.row.isSelf"
+                >
+                  <Icon icon="ep:edit" />删除
+                </el-button>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+        <el-form :inline="true" label-width="100px">
+          <el-form-item label="添加用户">
+            <el-select v-model="queryParams.addUserId" placeholder="请选择用户" style="width: 200px;" >
+              <el-option v-for="item in userOptions" :key="item.id" :label="item.nickname" :value="item.id" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="addQuery"><Icon icon="ep:plus" />添加</el-button>
+          </el-form-item>
+        </el-form>
+
+    </ContentWrap>
+    <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 { DisbursementApi } from '@/api/bpm/disbursement'
+import * as RoleApi from "@/api/system/role";
+import * as UserApi from "@/api/system/user";
+import  { ref }  from 'vue';
+
+/** 资金拨付流程表	 表单 */
+defineOptions({ name: 'AuthorityForm' })
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formRef = ref() // 表单 Ref
+const formData = ref({
+  addUserId : undefined,
+  projectId : undefined,
+  pageNo: 1,
+  pageSize: 10,
+  nickname: undefined,
+  mobile: undefined,
+  status: undefined,
+  deptId: undefined,
+  createTime: []
+})
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数
+const queryParams = reactive({
+  addUserId : undefined,
+  projectId : -1,
+  pageNo: 1,
+  pageSize: 10,
+  nickname: undefined,
+  mobile: undefined,
+  status: undefined,
+  deptId: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+const getList = async () => {
+  loading.value = true
+  try {
+      const data = await DisbursementApi.getTeamMember(queryParams)
+      console.log(data)
+      list.value = data.list
+      total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+const roleList = ref([] as RoleApi.RoleVO[]) // 角色的列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+/** 打开弹窗 */
+const open = async ( id : number) => {
+    queryParams.projectId = id
+    getList();
+    dialogVisible.value = true
+    resetForm()
+    // 修改时,设置数据
+    roleList.value = await RoleApi.getSimpleRoleList()
+    userOptions.value = await UserApi.getSimpleUserList()
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+    formLoading.value = true
+    try {
+        console.log(formData.value)
+
+        await DisbursementApi.setAuthority(formData.value);
+
+        message.success(t('common.createSuccess'))
+        dialogVisible.value = false
+        // 发送操作成功的事件
+        emit('success')
+    } finally {
+        formLoading.value = false
+    }
+}
+
+/** 重置按钮操作 */
+const addQuery = async () => {
+  await  DisbursementApi.addTeamMember(queryParams)
+  message.success(t('common.createSuccess'))
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 删除人员按钮操作 */
+const deleteQuery = async (id : any) => {
+  await DisbursementApi.deleteTeamMember(queryParams.projectId,id)
+  message.success("删除成功!")
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+const handlePermissionChange =async (row) =>{
+  loading.value = true
+  await DisbursementApi.updateTeamMember(row)
+  message.success("更新成功!")
+  loading.value = false
+}
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: -1 ,
+    roleIds: [],
+    userIds :[],
+    userIds :[],
+    readRoleIds: [],
+    readUserIds :[]
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 54 - 0
src/views/bpm/fund/apply/apply-detail.vue

@@ -0,0 +1,54 @@
+<template>
+  <ContentWrap>
+    <el-descriptions :column="1" border>
+      <el-descriptions-item label="资金类型">
+        <dict-tag :type="DICT_TYPE.BPM_FUND_APPLY_TYPE" :value="detailData.type" />
+      </el-descriptions-item>
+      <el-descriptions-item label="开始时间">
+        {{ formatDate(detailData.startTime, 'YYYY-MM-DD') }}
+      </el-descriptions-item>
+      <el-descriptions-item label="申请金额">
+        {{ detailData.applyAmount}}
+      </el-descriptions-item>
+<!--      <el-descriptions-item label="结束时间">-->
+<!--        {{ formatDate(detailData.endTime, 'YYYY-MM-DD') }}-->
+<!--      </el-descriptions-item>-->
+      <el-descriptions-item label="申请原因">
+        {{ detailData.reason }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { propTypes } from '@/utils/propTypes'
+import * as FundApi from '@/api/bpm/fund'
+
+defineOptions({ name: 'BpmFundApplyDetail' })
+
+const { query } = useRoute() // 查询参数
+
+const props = defineProps({
+  id: propTypes.number.def(undefined)
+})
+const detailLoading = ref(false) // 表单的加载中
+const detailData = ref<any>({}) // 详情数据
+const queryId = query.id as unknown as number // 从 URL 传递过来的 id 编号
+
+/** 获得数据 */
+const getInfo = async () => {
+  detailLoading.value = true
+  try {
+    detailData.value = await FundApi.getFundApply(props.id || queryId)
+  } finally {
+    detailLoading.value = false
+  }
+}
+defineExpose({ open: getInfo }) // 提供 open 方法,用于打开弹窗
+
+/** 初始化 **/
+onMounted(() => {
+  getInfo()
+})
+</script>

+ 257 - 0
src/views/bpm/fund/apply/apply-index.vue

@@ -0,0 +1,257 @@
+<template>
+<!--  <doc-alert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" />-->
+
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="资金类型" prop="type">
+        <el-select
+          v-model="queryParams.type"
+          class="!w-240px"
+          clearable
+          placeholder="请选择资金申请类型"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_FUND_APPLY_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="申请时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="审批结果" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          class="!w-240px"
+          clearable
+          placeholder="请选择审批结果"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="申请原因" prop="reason">
+        <el-input
+          v-model="queryParams.reason"
+          class="!w-240px"
+          clearable
+          placeholder="请输入资金申请原因"
+          @keyup.enter="handleQuery"
+        />
+      </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 plain type="primary" @click="handleCreate()">
+          <Icon class="mr-5px" icon="ep:plus" />
+          发起申请
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list">
+      <el-table-column align="center" label="申请编号" prop="id" />
+      <el-table-column align="center" label="状态" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="开始时间"
+        prop="startTime"
+        width="180"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="结束时间"
+        prop="endTime"
+        width="180"
+      />
+      <el-table-column align="center" label="资金类型" prop="type">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.BPM_FUND_APPLY_TYPE" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="申请原因" prop="reason" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="申请时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column align="center" label="操作" width="200">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['bpm:fund-apply:query']"
+            link
+            type="primary"
+            @click="handleDetail(scope.row)"
+          >
+            详情
+          </el-button>
+          <el-button
+            v-hasPermi="['bpm:fund-apply:query']"
+            link
+            type="primary"
+            @click="handleProcessDetail(scope.row)"
+          >
+            进度
+          </el-button>
+          <el-button
+            v-if="scope.row.result === 1"
+            v-hasPermi="['bpm:fund-apply:apply-create']"
+            link
+            type="danger"
+            @click="cancelFundApply(scope.row)"
+          >
+            取消
+          </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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as FundApi from '@/api/bpm/fund'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'BpmFundApply' })
+
+const message = useMessage() // 消息弹窗
+const router = useRouter() // 路由
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined,
+  status: undefined,
+  reason: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await FundApi.getFundApplyPage(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 handleCreate = () => {
+  router.push({ name: 'BpmFundApplyCreate' })
+}
+
+/** 详情操作 */
+const handleDetail = (row: FundApi.FundVO) => {
+  router.push({
+    name: 'BpmFundApplyDetail',
+    query: {
+      id: row.id
+    }
+  })
+}
+
+/** 取消请假操作 */
+const cancelFundApply = async (row) => {
+  // 二次确认
+  const { value } = await ElMessageBox.prompt('请输入取消原因', '取消流程', {
+    confirmButtonText: t('common.ok'),
+    cancelButtonText: t('common.cancel'),
+    inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
+    inputErrorMessage: '取消原因不能为空'
+  })
+  // 发起取消
+  await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
+  message.success('取消成功')
+  // 刷新列表
+  await getList()
+}
+
+/** 审批进度 */
+const handleProcessDetail = (row) => {
+  router.push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: row.processInstanceId
+    }
+  })
+}
+
+// fix: 列表不刷新的问题。
+watch(
+  () => router.currentRoute.value,
+  () => {
+    getList()
+  }
+)
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 159 - 0
src/views/mall/promotion/banner/BannerForm.vue

@@ -0,0 +1,159 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+    >
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="标题" prop="title">
+            <el-input v-model="formData.title" placeholder="请输入 Banner 标题" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="图片" prop="picUrl">
+            <UploadImg v-model="formData.picUrl" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="跳转地址" prop="url">
+            <el-input v-model="formData.url" placeholder="请输入跳转地址" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="排序" prop="sort">
+            <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <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-col :span="24">
+          <el-form-item label="位置" prop="position">
+            <el-radio-group v-model="formData.position">
+              <el-radio
+                v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_BANNER_POSITION)"
+                :key="dict.value"
+                :label="dict.value"
+              >
+                {{ dict.label }}
+              </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="描述" prop="memo">
+            <el-input v-model="formData.memo" placeholder="请输入描述" type="textarea" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as BannerApi from '@/api/mall/market/banner'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  title: undefined,
+  picUrl: undefined,
+  status: 0,
+  position: 1,
+  url: undefined,
+  sort: 0,
+  memo: undefined
+})
+const formRules = reactive({
+  title: [{ required: true, message: 'Banner 标题不能为空', trigger: 'blur' }],
+  picUrl: [{ required: true, message: '图片 URL 不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '活动状态不能为空', trigger: 'blur' }],
+  position: [{ required: true, message: '位置不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
+  url: [{ 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 BannerApi.getBanner(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as BannerApi.BannerVO
+    if (formType.value === 'create') {
+      await BannerApi.createBanner(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await BannerApi.updateBanner(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    title: undefined,
+    picUrl: undefined,
+    status: 0,
+    position: 1,
+    url: undefined,
+    sort: 0,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 233 - 0
src/views/mall/promotion/bargain/activity/BargainActivityForm.vue

@@ -0,0 +1,233 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+    <Form
+      ref="formRef"
+      v-loading="formLoading"
+      :is-col="true"
+      :rules="rules"
+      :schema="allSchemas.formSchema"
+      class="mt-10px"
+    >
+      <template #spuId>
+        <el-button @click="spuSelectRef.open()">选择商品</el-button>
+        <SpuAndSkuList
+          ref="spuAndSkuListRef"
+          :rule-config="ruleConfig"
+          :spu-list="spuList"
+          :spu-property-list-p="spuPropertyList"
+        >
+          <el-table-column align="center" label="砍价起始价格(元)" min-width="168">
+            <template #default="{ row: sku }">
+              <el-input-number
+                v-model="sku.productConfig.bargainFirstPrice"
+                :min="0"
+                :precision="2"
+                :step="0.1"
+                class="w-100%"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="砍价底价(元)" min-width="168">
+            <template #default="{ row: sku }">
+              <el-input-number
+                v-model="sku.productConfig.bargainMinPrice"
+                :min="0"
+                :precision="2"
+                :step="0.1"
+                class="w-100%"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="活动库存" min-width="168">
+            <template #default="{ row: sku }">
+              <el-input-number v-model="sku.productConfig.stock" class="w-100%" />
+            </template>
+          </el-table-column>
+        </SpuAndSkuList>
+      </template>
+    </Form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+  <SpuSelect ref="spuSelectRef" :isSelectSku="true" :radio="true" @confirm="selectSpu" />
+</template>
+<script lang="ts" setup>
+import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivity'
+import { BargainProductVO } from '@/api/mall/promotion/bargain/bargainActivity'
+import { allSchemas, rules } from './bargainActivity.data'
+import { SpuAndSkuList, SpuProperty, SpuSelect } from '@/views/mall/promotion/components'
+import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { convertToInteger, formatToFraction } from '@/utils'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'PromotionBargainActivityForm' })
+
+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 formRef = ref() // 表单 Ref
+
+// ================= 商品选择相关 =================
+
+const spuSelectRef = ref() // 商品和属性选择 Ref
+const spuAndSkuListRef = ref() // sku 秒杀配置组件Ref
+const spuList = ref<BargainActivityApi.SpuExtension[]>([]) // 选择的 spu
+const spuPropertyList = ref<SpuProperty<BargainActivityApi.SpuExtension>[]>([])
+const ruleConfig: RuleConfig[] = [
+  {
+    name: 'productConfig.bargainFirstPrice',
+    rule: (arg) => arg > 0,
+    message: '商品砍价起始价格不能小于 0 !!!'
+  },
+  {
+    name: 'productConfig.bargainMinPrice',
+    rule: (arg) => arg >= 0,
+    message: '商品砍价底价不能小于 0 !!!'
+  },
+  {
+    name: 'productConfig.stock',
+    rule: (arg) => arg >= 1,
+    message: '商品活动库存不能小于 1 !!!'
+  }
+]
+const selectSpu = (spuId: number, skuIds: number[]) => {
+  formRef.value.setValues({ spuId })
+  getSpuDetails(spuId, skuIds)
+}
+/**
+ * 获取 SPU 详情
+ */
+const getSpuDetails = async (
+  spuId: number,
+  skuIds: number[] | undefined,
+  products?: BargainProductVO[]
+) => {
+  const spuProperties: SpuProperty<BargainActivityApi.SpuExtension>[] = []
+  const res = (await ProductSpuApi.getSpuDetailList([spuId])) as BargainActivityApi.SpuExtension[]
+  if (res.length == 0) {
+    return
+  }
+  spuList.value = []
+  // 因为只能选择一个
+  const spu = res[0]
+  const selectSkus =
+    typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
+  selectSkus?.forEach((sku) => {
+    let config: BargainProductVO = {
+      spuId: spu.id!,
+      skuId: sku.id!,
+      bargainFirstPrice: 1,
+      bargainMinPrice: 1,
+      stock: 1
+    }
+    if (typeof products !== 'undefined') {
+      const product = products.find((item) => item.skuId === sku.id)
+      if (product) {
+        product.bargainFirstPrice = formatToFraction(product.bargainFirstPrice)
+        product.bargainMinPrice = formatToFraction(product.bargainMinPrice)
+      }
+      config = product || config
+    }
+    sku.productConfig = config
+  })
+  spu.skus = selectSkus as BargainActivityApi.SkuExtension[]
+  spuProperties.push({
+    spuId: spu.id!,
+    spuDetail: spu,
+    propertyList: getPropertyList(spu)
+  })
+  spuList.value.push(spu)
+  spuPropertyList.value = spuProperties
+}
+
+// ================= end =================
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  await resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      const data = (await BargainActivityApi.getBargainActivity(
+        id
+      )) as BargainActivityApi.BargainActivityVO
+      // 用户每次砍价金额分转元, 分转元
+      data.randomMinPrice = formatToFraction(data.randomMinPrice)
+      data.randomMaxPrice = formatToFraction(data.randomMaxPrice)
+      // 对齐活动商品处理结构
+      await getSpuDetails(
+        data.spuId!,
+        [data.skuId],
+        [
+          {
+            spuId: data.spuId!,
+            skuId: data.skuId,
+            bargainFirstPrice: data.bargainFirstPrice, // 砍价起始价格,单位分
+            bargainMinPrice: data.bargainMinPrice, // 砍价底价
+            stock: data.stock // 活动库存
+          }
+        ]
+      )
+      formRef.value.setValues(data)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 重置表单 */
+const resetForm = async () => {
+  spuList.value = []
+  spuPropertyList.value = []
+  await nextTick()
+  formRef.value.getElFormRef().resetFields()
+}
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.getElFormRef().validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = cloneDeep(formRef.value.formModel) as BargainActivityApi.BargainActivityVO
+    const products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
+    products.forEach((item: BargainProductVO) => {
+      // 砍价价格元转分
+      item.bargainFirstPrice = convertToInteger(item.bargainFirstPrice)
+      item.bargainMinPrice = convertToInteger(item.bargainMinPrice)
+    })
+    // 用户每次砍价金额分转元, 元转分
+    data.randomMinPrice = convertToInteger(data.randomMinPrice)
+    data.randomMaxPrice = convertToInteger(data.randomMaxPrice)
+    const formData = { ...data, ...products[0] }
+    if (formType.value === 'create') {
+      await BargainActivityApi.createBargainActivity(formData)
+      message.success(t('common.createSuccess'))
+    } else {
+      await BargainActivityApi.updateBargainActivity(formData)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+</script>

+ 146 - 0
src/views/mall/promotion/bargain/activity/bargainActivity.data.ts

@@ -0,0 +1,146 @@
+import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
+import { dateFormatter2 } from '@/utils/formatTime'
+
+// 表单校验
+export const rules = reactive({
+  name: [required],
+  startTime: [required],
+  endTime: [required],
+  helpMaxCount: [required],
+  bargainCount: [required],
+  singleLimitCount: [required]
+})
+
+// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
+const crudSchemas = reactive<CrudSchema[]>([
+  {
+    label: '砍价活动名称',
+    field: 'name',
+    isSearch: true,
+    isTable: false,
+    form: {
+      colProps: {
+        span: 24
+      }
+    }
+  },
+  {
+    label: '活动开始时间',
+    field: 'startTime',
+    formatter: dateFormatter2,
+    isSearch: true,
+    search: {
+      component: 'DatePicker',
+      componentProps: {
+        valueFormat: 'YYYY-MM-DD',
+        type: 'daterange'
+      }
+    },
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'date',
+        valueFormat: 'x'
+      }
+    },
+    table: {
+      width: 120
+    }
+  },
+  {
+    label: '活动结束时间',
+    field: 'endTime',
+    formatter: dateFormatter2,
+    isSearch: true,
+    search: {
+      component: 'DatePicker',
+      componentProps: {
+        valueFormat: 'YYYY-MM-DD',
+        type: 'daterange'
+      }
+    },
+    form: {
+      component: 'DatePicker',
+      componentProps: {
+        type: 'date',
+        valueFormat: 'x'
+      }
+    },
+    table: {
+      width: 120
+    }
+  },
+  {
+    label: '砍价人数',
+    field: 'helpMaxCount',
+    isSearch: false,
+    form: {
+      component: 'InputNumber',
+      labelMessage: '参与人数不能少于两人',
+      value: 2
+    }
+  },
+  {
+    label: '最大帮砍次数',
+    field: 'bargainCount',
+    isSearch: false,
+    form: {
+      component: 'InputNumber',
+      labelMessage: '参与人数不能少于两人',
+      value: 2
+    }
+  },
+  {
+    label: '总限购数量',
+    field: 'totalLimitCount',
+    isSearch: false,
+    form: {
+      component: 'InputNumber',
+      labelMessage: '用户最大能发起砍价的次数',
+      value: 0
+    }
+  },
+  {
+    label: '砍价的最小金额',
+    field: 'randomMinPrice',
+    isSearch: false,
+    isTable: false,
+    form: {
+      component: 'InputNumber',
+      componentProps: {
+        min: 0,
+        precision: 2,
+        step: 0.1
+      },
+      labelMessage: '用户每次砍价的最小金额',
+      value: 0
+    }
+  },
+  {
+    label: '砍价的最大金额',
+    field: 'randomMaxPrice',
+    isSearch: false,
+    isTable: false,
+    form: {
+      component: 'InputNumber',
+      componentProps: {
+        min: 0,
+        precision: 2,
+        step: 0.1
+      },
+      labelMessage: '用户每次砍价的最大金额',
+      value: 0
+    }
+  },
+  {
+    label: '砍价商品',
+    field: 'spuId',
+    isSearch: false,
+    form: {
+      colProps: {
+        span: 24
+      }
+    }
+  }
+])
+export const { allSchemas } = useCrudSchemas(crudSchemas)

BIN
src/views/mall/promotion/kefu/components/asserts/baiyan.png


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

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

+ 72 - 0
src/views/system/area/AreaForm.vue

@@ -0,0 +1,72 @@
+<template>
+  <Dialog v-model="dialogVisible" title="IP 查询">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="80px"
+    >
+      <el-form-item label="IP" prop="ip">
+        <el-input v-model="formData.ip" placeholder="请输入 IP 地址" />
+      </el-form-item>
+      <el-form-item label="地址" prop="result">
+        <el-input v-model="formData.result" placeholder="展示查询 IP 结果" readonly />
+      </el-form-item>
+    </el-form>
+    <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 AreaApi from '@/api/system/area'
+
+defineOptions({ name: 'SystemAreaForm' })
+
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:提交的按钮禁用
+const formData = ref({
+  ip: '',
+  result: undefined
+})
+const formRules = reactive({
+  ip: [{ required: true, message: 'IP 地址不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async () => {
+  dialogVisible.value = true
+  resetForm()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    formData.value.result = await AreaApi.getAreaByIp(formData.value.ip!.trim())
+    message.success('查询成功')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    ip: '',
+    result: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>