mirror of
https://gitee.com/hhyykk/ipms-sjy-ui.git
synced 2025-07-23 23:35:06 +08:00
Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3
This commit is contained in:
@ -1,142 +1,279 @@
|
||||
<template>
|
||||
<Form
|
||||
<el-form
|
||||
v-show="getShow"
|
||||
:rules="rules"
|
||||
:schema="schema"
|
||||
class="w-[100%] dark:(border-1 border-[var(--el-border-color)] border-solid)"
|
||||
hide-required-asterisk
|
||||
ref="formLogin"
|
||||
:model="registerData.registerForm"
|
||||
:rules="registerRules"
|
||||
class="login-form"
|
||||
label-position="top"
|
||||
label-width="120px"
|
||||
size="large"
|
||||
@register="register"
|
||||
>
|
||||
<template #title>
|
||||
<LoginFormTitle style="width: 100%" />
|
||||
</template>
|
||||
|
||||
<template #code="form">
|
||||
<div class="w-[100%] flex">
|
||||
<el-input v-model="form['code']" :placeholder="t('login.codePlaceholder')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #register>
|
||||
<div class="w-[100%]">
|
||||
<XButton
|
||||
:loading="loading"
|
||||
:title="t('login.register')"
|
||||
class="w-[100%]"
|
||||
type="primary"
|
||||
@click="loginRegister()"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-15px w-[100%]">
|
||||
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
<el-row style="margin-right: -10px; margin-left: -10px">
|
||||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||
<el-form-item>
|
||||
<LoginFormTitle style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||
<el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName">
|
||||
<el-input
|
||||
v-model="registerData.registerForm.tenantName"
|
||||
:placeholder="t('login.tenantname')"
|
||||
:prefix-icon="iconHouse"
|
||||
link
|
||||
type="primary"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="registerData.registerForm.username"
|
||||
:placeholder="t('login.username')"
|
||||
size="large"
|
||||
:prefix-icon="iconAvatar"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="registerData.registerForm.nickname"
|
||||
placeholder="昵称"
|
||||
size="large"
|
||||
:prefix-icon="iconAvatar"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="registerData.registerForm.password"
|
||||
type="password"
|
||||
auto-complete="off"
|
||||
:placeholder="t('login.password')"
|
||||
size="large"
|
||||
:prefix-icon="iconLock"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="registerData.registerForm.confirmPassword"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
:placeholder="t('login.checkPassword')"
|
||||
:prefix-icon="iconLock"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||
<el-form-item>
|
||||
<XButton
|
||||
:loading="loginLoading"
|
||||
:title="t('login.register')"
|
||||
class="w-[100%]"
|
||||
type="primary"
|
||||
@click="getCode()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<Verify
|
||||
ref="verify"
|
||||
:captchaType="captchaType"
|
||||
:imgSize="{ width: '400px', height: '200px' }"
|
||||
mode="pop"
|
||||
@success="handleRegister"
|
||||
/>
|
||||
</el-row>
|
||||
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
|
||||
</el-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { FormRules } from 'element-plus'
|
||||
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { useValidator } from '@/hooks/web/useValidator'
|
||||
import { ElLoading } from 'element-plus'
|
||||
import LoginFormTitle from './LoginFormTitle.vue'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { useIcon } from '@/hooks/web/useIcon'
|
||||
import * as authUtil from '@/utils/auth'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
import * as LoginApi from '@/api/login'
|
||||
import { LoginStateEnum, useLoginState } from './useLogin'
|
||||
import { FormSchema } from '@/types/form'
|
||||
|
||||
defineOptions({ name: 'RegisterForm' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const { required } = useValidator()
|
||||
const { register, elFormRef } = useForm()
|
||||
const iconHouse = useIcon({ icon: 'ep:house' })
|
||||
const iconAvatar = useIcon({ icon: 'ep:avatar' })
|
||||
const iconLock = useIcon({ icon: 'ep:lock' })
|
||||
const formLogin = ref()
|
||||
const { handleBackLogin, getLoginState } = useLoginState()
|
||||
const { currentRoute, push } = useRouter()
|
||||
const permissionStore = usePermissionStore()
|
||||
const redirect = ref<string>('')
|
||||
const loginLoading = ref(false)
|
||||
const verify = ref()
|
||||
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
|
||||
|
||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
|
||||
|
||||
const schema = reactive<FormSchema[]>([
|
||||
{
|
||||
field: 'title',
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
label: t('login.username'),
|
||||
value: '',
|
||||
component: 'Input',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: t('login.usernamePlaceholder')
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'password',
|
||||
label: t('login.password'),
|
||||
value: '',
|
||||
component: 'InputPassword',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
},
|
||||
strength: true,
|
||||
placeholder: t('login.passwordPlaceholder')
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'check_password',
|
||||
label: t('login.checkPassword'),
|
||||
value: '',
|
||||
component: 'InputPassword',
|
||||
colProps: {
|
||||
span: 24
|
||||
},
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '100%'
|
||||
},
|
||||
strength: true,
|
||||
placeholder: t('login.passwordPlaceholder')
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
label: t('login.code'),
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'register',
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
const equalToPassword = (rule, value, callback) => {
|
||||
if (registerData.registerForm.password !== value) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
])
|
||||
|
||||
const rules: FormRules = {
|
||||
username: [required()],
|
||||
password: [required()],
|
||||
check_password: [required()],
|
||||
code: [required()]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const registerRules = {
|
||||
tenantName: [
|
||||
{ required: true, trigger: 'blur', message: '请输入您所属的租户' },
|
||||
{ min: 2, max: 20, message: '租户账号长度必须介于 2 和 20 之间', trigger: 'blur' }
|
||||
],
|
||||
username: [
|
||||
{ required: true, trigger: 'blur', message: '请输入您的账号' },
|
||||
{ min: 4, max: 30, message: '用户账号长度必须介于 4 和 30 之间', trigger: 'blur' }
|
||||
],
|
||||
nickname: [
|
||||
{ required: true, trigger: 'blur', message: '请输入您的昵称' },
|
||||
{ min: 0, max: 30, message: '昵称长度必须介于 0 和 30 之间', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, trigger: 'blur', message: '请输入您的密码' },
|
||||
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' },
|
||||
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\\ |', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, trigger: 'blur', message: '请再次输入您的密码' },
|
||||
{ required: true, validator: equalToPassword, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const loginRegister = async () => {
|
||||
const formRef = unref(elFormRef)
|
||||
formRef?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
loading.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
const registerData = reactive({
|
||||
isShowPassword: false,
|
||||
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
||||
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
||||
registerForm: {
|
||||
tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
|
||||
nickname: '',
|
||||
tenantId: 0,
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
captchaVerification: ''
|
||||
}
|
||||
})
|
||||
|
||||
// 提交注册
|
||||
const handleRegister = async (params: any) => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (registerData.tenantEnable) {
|
||||
await getTenantId()
|
||||
registerData.registerForm.tenantId = authUtil.getTenantId()
|
||||
}
|
||||
})
|
||||
|
||||
if (registerData.captchaEnable) {
|
||||
registerData.registerForm.captchaVerification = params.captchaVerification
|
||||
}
|
||||
|
||||
const res = await LoginApi.register(registerData.registerForm)
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
loading.value = ElLoading.service({
|
||||
lock: true,
|
||||
text: '正在加载系统中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
|
||||
authUtil.removeLoginForm()
|
||||
|
||||
authUtil.setToken(res)
|
||||
if (!redirect.value) {
|
||||
redirect.value = '/'
|
||||
}
|
||||
// 判断是否为SSO登录
|
||||
if (redirect.value.indexOf('sso') !== -1) {
|
||||
window.location.href = window.location.href.replace('/login?redirect=', '')
|
||||
} else {
|
||||
push({ path: redirect.value || permissionStore.addRouters[0].path })
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
loading.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
const getCode = async () => {
|
||||
// 情况一,未开启:则直接注册
|
||||
if (registerData.captchaEnable === 'false') {
|
||||
await handleRegister({})
|
||||
} else {
|
||||
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行注册
|
||||
// 弹出验证码
|
||||
verify.value.show()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取租户 ID
|
||||
const getTenantId = async () => {
|
||||
if (registerData.tenantEnable === 'true') {
|
||||
const res = await LoginApi.getTenantIdByName(registerData.registerForm.tenantName)
|
||||
authUtil.setTenantId(res)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据域名,获得租户信息
|
||||
const getTenantByWebsite = async () => {
|
||||
const website = location.host
|
||||
const res = await LoginApi.getTenantByWebsite(website)
|
||||
if (res) {
|
||||
registerData.registerForm.tenantName = res.name
|
||||
authUtil.setTenantId(res.id)
|
||||
}
|
||||
}
|
||||
const loading = ref() // ElLoading.service 返回的实例
|
||||
|
||||
watch(
|
||||
() => currentRoute.value,
|
||||
(route: RouteLocationNormalizedLoaded) => {
|
||||
redirect.value = route?.query?.redirect as string
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
onMounted(() => {
|
||||
// getCookie()
|
||||
getTenantByWebsite()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.anticon) {
|
||||
&:hover {
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login-code {
|
||||
float: right;
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 100px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
151
src/views/knowledge/dataset-form/form-step1.vue
Normal file
151
src/views/knowledge/dataset-form/form-step1.vue
Normal file
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<!-- 标题 -->
|
||||
<div class="title">
|
||||
<div>选择数据源</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据源选择 -->
|
||||
<div class="resource-btn" >导入已有文本</div>
|
||||
|
||||
<!-- 上传文件区域 -->
|
||||
<el-form>
|
||||
<div class="upload-section">
|
||||
<div class="upload-label">上传文本文件</div>
|
||||
<el-upload
|
||||
class="upload-area"
|
||||
action="#"
|
||||
:file-list="fileList"
|
||||
:on-remove="handleRemove"
|
||||
:before-upload="beforeUpload"
|
||||
list-type="text"
|
||||
drag
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">拖拽文件至此,或者 <em>选择文件</em></div>
|
||||
<div class="el-upload__tip">
|
||||
已支持 TXT、MARKDOWN、PDF、HTML、XLSX、XLS、DOCX、CSV、EML、MSG、PPTX、PPT、XML、EPUB,每个文件不超过 15MB。
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<!-- 下一步按钮 -->
|
||||
<div class="next-button">
|
||||
<el-button type="primary" :disabled="!fileList.length">下一步</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 知识库创建 -->
|
||||
<div class="create-knowledge">
|
||||
<el-link type="primary" underline>创建一个空知识库</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const fileList = ref([])
|
||||
|
||||
const handleRemove = (file, fileList) => {
|
||||
console.log(file, fileList)
|
||||
}
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
fileList.value.push(file)
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload-container {
|
||||
width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebebeb;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.resource-btn {
|
||||
margin-top: 20px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
width: 150px;
|
||||
border: 1.5px solid #528bff;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin: 20px 0;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.upload-label {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
margin-top: 10px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.el-upload__text em {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
text-align: left;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.create-knowledge {
|
||||
text-align: left;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.source-radio-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.el-radio-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.el-radio-button .el-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
168
src/views/knowledge/dataset-form/form-step2.vue
Normal file
168
src/views/knowledge/dataset-form/form-step2.vue
Normal file
@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<el-row>
|
||||
<!-- Left Section -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<!-- 分段设置 -->
|
||||
<el-form>
|
||||
<el-form-item label="分段设置">
|
||||
<el-radio-group v-model="segmentSetting">
|
||||
<el-radio label="自动分段与清洗">自动分段与清洗</el-radio>
|
||||
<el-radio label="自定义">自定义</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 索引方式 -->
|
||||
<el-form-item label="索引方式">
|
||||
<el-radio-group v-model="indexingMethod">
|
||||
<el-radio label="高质量">高质量</el-radio>
|
||||
<el-radio label="经济">经济</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- Embedding 模型 -->
|
||||
<el-form-item label="Embedding 模型">
|
||||
<el-select v-model="embeddingModel" placeholder="Select Embedding Model">
|
||||
<el-option label="text-embedding-3-large" value="text-embedding-3-large"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 检索设置 -->
|
||||
<el-form-item label="检索设置">
|
||||
<el-card style="width: 400px;">
|
||||
<div class="card-header">
|
||||
<span>向量检索</span>
|
||||
</div>
|
||||
<el-slider v-model="topK" :min="1" :max="10" label="Top K"/>
|
||||
<el-slider v-model="scoreThreshold" :min="0" :max="1" step="0.1" label="Score 阈值"/>
|
||||
</el-card>
|
||||
|
||||
<el-card style="width: 400px;">
|
||||
<div class="card-header">
|
||||
<span>全文检索</span>
|
||||
</div>
|
||||
<el-slider v-model="topK" :min="1" :max="10" label="Top K"/>
|
||||
<el-slider v-model="scoreThreshold" :min="0" :max="1" step="0.1" label="Score 阈值"/>
|
||||
</el-card>
|
||||
|
||||
<el-card style="width: 400px;">
|
||||
<div class="card-header">
|
||||
<span>混合检索</span>
|
||||
</div>
|
||||
<el-slider v-model="topK" :min="1" :max="10" label="Top K"/>
|
||||
<el-slider v-model="scoreThreshold" :min="0" :max="1" step="0.1" label="Score 阈值"/>
|
||||
</el-card>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- Right Section: 分段预览 -->
|
||||
<el-col :span="9">
|
||||
<el-card shadow="never">
|
||||
<div class="previews-title">分段预览</div>
|
||||
<template v-for="(segment, index) in segmentPreviews" :key="index">
|
||||
<div class="segment-preview">
|
||||
<div class="title">
|
||||
<div class="left">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4.74999 1.5L3.24999 10.5M8.74998 1.5L7.24998 10.5M10.25 4H1.75M9.75 8H1.25"
|
||||
stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span class="id">{{ segment.number }}</span>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4 3.5H8M6 3.5V8.5M3.9 10.5H8.1C8.94008 10.5 9.36012 10.5 9.68099 10.3365C9.96323 10.1927 10.1927 9.96323 10.3365 9.68099C10.5 9.36012 10.5 8.94008 10.5 8.1V3.9C10.5 3.05992 10.5 2.63988 10.3365 2.31901C10.1927 2.03677 9.96323 1.8073 9.68099 1.66349C9.36012 1.5 8.94008 1.5 8.1 1.5H3.9C3.05992 1.5 2.63988 1.5 2.31901 1.66349C2.03677 1.8073 1.8073 2.03677 1.66349 2.31901C1.5 2.63988 1.5 3.05992 1.5 3.9V8.1C1.5 8.94008 1.5 9.36012 1.66349 9.68099C1.8073 9.96323 2.03677 10.1927 2.31901 10.3365C2.63988 10.5 3.05992 10.5 3.9 10.5Z"
|
||||
stroke="#667085" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span class="char-size">7777 字符</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">{{ segment.text }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
|
||||
// Reactive variables for form control
|
||||
const segmentSetting = ref('自动分段与清洗');
|
||||
const indexingMethod = ref('高质量');
|
||||
const embeddingModel = ref('text-embedding-3-large');
|
||||
const directionalSearch = ref(true);
|
||||
const topK = ref(3);
|
||||
const scoreThreshold = ref(0.5);
|
||||
|
||||
// Mock data for segment previews
|
||||
const segmentPreviews = ref([
|
||||
{number: '001', text: "同步obs模型...'UAE-large-V1'"},
|
||||
{number: '002', text: "同步obs模型...'plip'"},
|
||||
{number: '003', text: "同步obs模型...'phoBERT-base-v2'"},
|
||||
{number: '004', text: "同步obs模型...'lama3-bb-bnb-4bit'"},
|
||||
{number: '005', text: "同步obs模型...'t5-base-split-and-rephrase'"}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* Add any custom styles here */
|
||||
|
||||
.previews-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.segment-preview {
|
||||
background-color: rgba(228, 228, 228, 0.38);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.left {
|
||||
border-right: 5px;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
color: #676767;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
|
||||
.id {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.char-size {
|
||||
margin-left: 5px;
|
||||
font-size: 13px;
|
||||
color: rgba(57, 57, 57, 0.66);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
152
src/views/knowledge/dataset.vue
Normal file
152
src/views/knowledge/dataset.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="knowledge-base-container">
|
||||
<div class="card-container">
|
||||
<el-card class="create-card" shadow="hover">
|
||||
<div class="create-content">
|
||||
<el-icon class="create-icon"><Plus /></el-icon>
|
||||
<span class="create-text">创建知识库</span>
|
||||
</div>
|
||||
<div class="create-footer">
|
||||
导入您自己的文本数据或通过 Webhook 实时写入数据以增强 LLM 的上下文。
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="document-card" shadow="hover" v-for="index in 4" :key="index">
|
||||
<div class="document-header">
|
||||
<el-icon><Folder /></el-icon>
|
||||
<span>接口鉴权示例代码.md</span>
|
||||
</div>
|
||||
<div class="document-info">
|
||||
<el-tag size="small">1 文档</el-tag>
|
||||
<el-tag size="small" type="info">5 千字符</el-tag>
|
||||
<el-tag size="small" type="warning">0 关联应用</el-tag>
|
||||
</div>
|
||||
<p class="document-description">
|
||||
useful for when you want to answer queries about the 接口鉴权示例代码.md
|
||||
</p>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 30, 40]"
|
||||
:small="false"
|
||||
:disabled="false"
|
||||
:background="true"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Folder, Plus } from '@element-plus/icons-vue'
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(100) // 假设总共有100条数据
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
console.log(`每页 ${val} 条`)
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
console.log(`当前页: ${val}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.knowledge-base-container {
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
position: absolute;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
bottom: 40px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* Enable wrapping */
|
||||
gap: 20px;
|
||||
margin-bottom: auto; /* Pushes pagination to the bottom */
|
||||
}
|
||||
|
||||
.create-card, .document-card {
|
||||
flex: 1 1 360px; /* Allow cards to grow and shrink */
|
||||
min-width: 0;
|
||||
max-width: 400px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.create-card {
|
||||
background-color: rgba(168, 168, 168, 0.22);
|
||||
}
|
||||
.create-card:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.create-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.create-icon {
|
||||
font-size: 24px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.create-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.create-footer {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.document-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.document-info {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.document-description {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
@ -105,7 +105,7 @@ const list = ref([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
propertyId: Number(params.propertyId),
|
||||
propertyId: params.propertyId,
|
||||
name: undefined
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
@ -180,17 +180,17 @@
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="销售价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.price) }}
|
||||
{{ row.price }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="市场价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.marketPrice) }}
|
||||
{{ row.marketPrice }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="成本价(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.costPrice) }}
|
||||
{{ row.costPrice }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="库存" min-width="80">
|
||||
@ -211,12 +211,12 @@
|
||||
<template v-if="formData!.subCommissionType">
|
||||
<el-table-column align="center" label="一级返佣(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.firstBrokeragePrice) }}
|
||||
{{ row.firstBrokeragePrice }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="二级返佣(元)" min-width="80">
|
||||
<template #default="{ row }">
|
||||
{{ formatToFraction(row.secondBrokeragePrice) }}
|
||||
{{ row.secondBrokeragePrice }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
@ -45,7 +45,7 @@
|
||||
:show-word-limit="true"
|
||||
class="w-80!"
|
||||
maxlength="128"
|
||||
placeholder="请输入商品名称"
|
||||
placeholder="请输入商品简介"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
@ -4,27 +4,27 @@
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="活动名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入活动名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入活动名称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择活动状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请选择活动状态"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
@ -35,15 +35,22 @@
|
||||
</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 @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
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['promotion:combination-activity:create']"
|
||||
plain
|
||||
type="primary"
|
||||
@click="openForm('create')"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
<Icon class="mr-5px" icon="ep:plus" />
|
||||
新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@ -51,77 +58,77 @@
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="活动编号" prop="id" min-width="80" />
|
||||
<el-table-column label="活动名称" prop="name" min-width="140" />
|
||||
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
|
||||
<el-table-column label="活动编号" min-width="80" prop="id" />
|
||||
<el-table-column label="活动名称" min-width="140" prop="name" />
|
||||
<el-table-column label="活动时间" min-width="210">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
|
||||
~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品图片" prop="spuName" min-width="80">
|
||||
<el-table-column label="商品图片" min-width="80" prop="spuName">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
:preview-src-list="[scope.row.picUrl]"
|
||||
:src="scope.row.picUrl"
|
||||
class="h-40px w-40px"
|
||||
:preview-src-list="[scope.row.picUrl]"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品标题" prop="spuName" min-width="300" />
|
||||
<el-table-column label="商品标题" min-width="300" prop="spuName" />
|
||||
<el-table-column
|
||||
label="原价"
|
||||
prop="marketPrice"
|
||||
min-width="100"
|
||||
:formatter="fenToYuanFormat"
|
||||
label="原价"
|
||||
min-width="100"
|
||||
prop="marketPrice"
|
||||
/>
|
||||
<el-table-column label="拼团价" prop="seckillPrice" min-width="100">
|
||||
<el-table-column label="拼团价" min-width="100" prop="seckillPrice">
|
||||
<template #default="scope">
|
||||
{{ formatCombinationPrice(scope.row.products) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开团组数" prop="groupCount" min-width="100" />
|
||||
<el-table-column label="成团组数" prop="groupSuccessCount" min-width="100" />
|
||||
<el-table-column label="购买次数" prop="recordCount" min-width="100" />
|
||||
<el-table-column label="活动状态" align="center" prop="status" min-width="100">
|
||||
<el-table-column label="开团组数" min-width="100" prop="groupCount" />
|
||||
<el-table-column label="成团组数" min-width="100" prop="groupSuccessCount" />
|
||||
<el-table-column label="购买次数" min-width="100" prop="recordCount" />
|
||||
<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
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="操作" align="center" width="150px" fixed="right">
|
||||
<el-table-column align="center" fixed="right" label="操作" width="150px">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['promotion:combination-activity:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['promotion:combination-activity:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 0"
|
||||
v-hasPermi="['promotion:combination-activity:close']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleClose(scope.row.id)"
|
||||
v-if="scope.row.status === 0"
|
||||
v-hasPermi="['promotion:combination-activity:close']"
|
||||
>
|
||||
关闭
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
v-hasPermi="['promotion:combination-activity:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-else
|
||||
v-hasPermi="['promotion:combination-activity:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
@ -130,9 +137,9 @@
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
@ -141,12 +148,11 @@
|
||||
<CombinationActivityForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { dateFormatter, formatDate } from '@/utils/formatTime'
|
||||
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
|
||||
import CombinationActivityForm from './CombinationActivityForm.vue'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { fenToYuanFormat } from '@/utils/formatter'
|
||||
import { fenToYuan } from '@/utils'
|
||||
|
||||
@ -165,7 +171,6 @@ const queryParams = reactive({
|
||||
status: null
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
@ -197,12 +202,11 @@ const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
// TODO 芋艿:这里要改下
|
||||
/** 关闭按钮操作 */
|
||||
const handleClose = async (id: number) => {
|
||||
try {
|
||||
// 关闭的二次确认
|
||||
await message.confirm('确认关闭该秒杀活动吗?')
|
||||
await message.confirm('确认关闭该拼团活动吗?')
|
||||
// 发起关闭
|
||||
await CombinationActivityApi.closeCombinationActivity(id)
|
||||
message.success('关闭成功')
|
||||
|
@ -30,13 +30,13 @@
|
||||
<el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
|
||||
<el-table-column align="center" label="库存" min-width="90" prop="stock" />
|
||||
<el-table-column
|
||||
v-if="spuData.length > 1 && isDelete"
|
||||
v-if="spuData.length > 1 && deletable"
|
||||
align="center"
|
||||
label="操作"
|
||||
min-width="90"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link @click="deleteSpu(scope.row.id)"> 删除 </el-button>
|
||||
<el-button link type="primary" @click="deleteSpu(scope.row.id)"> 删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -56,13 +56,13 @@ const props = defineProps<{
|
||||
spuList: T[]
|
||||
ruleConfig: RuleConfig[]
|
||||
spuPropertyListP: SpuProperty<T>[]
|
||||
isDelete?: boolean // SPU 是否可删除;TODO deletable 换成这个名字好点。
|
||||
deletable?: boolean // SPU 是否可删除;
|
||||
}>()
|
||||
|
||||
const spuData = ref<Spu[]>([]) // spu 详情数据列表
|
||||
const skuListRef = ref() // 商品属性列表Ref
|
||||
const spuPropertyList = ref<SpuProperty<T>[]>([]) // spuId 对应的 sku 的属性列表
|
||||
const expandRowKeys = ref<number[]>() // 控制展开行需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。
|
||||
const expandRowKeys = ref<string[]>([]) // 控制展开行需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。
|
||||
|
||||
/**
|
||||
* 获取所有 sku 活动配置
|
||||
@ -71,10 +71,10 @@ const expandRowKeys = ref<number[]>() // 控制展开行需要设置 row-key 属
|
||||
*/
|
||||
const getSkuConfigs = (extendedAttribute: string) => {
|
||||
skuListRef.value.validateSku()
|
||||
const seckillProducts = []
|
||||
const seckillProducts: any[] = []
|
||||
spuPropertyList.value.forEach((item) => {
|
||||
item.spuDetail.skus.forEach((sku) => {
|
||||
seckillProducts.push(sku[extendedAttribute])
|
||||
item.spuDetail.skus?.forEach((sku: any) => {
|
||||
seckillProducts.push(sku[extendedAttribute] as any)
|
||||
})
|
||||
})
|
||||
return seckillProducts
|
||||
@ -124,10 +124,10 @@ watch(
|
||||
() => props.spuPropertyListP,
|
||||
(data) => {
|
||||
if (!data) return
|
||||
spuPropertyList.value = data as SpuProperty<T>[]
|
||||
spuPropertyList.value = data as SpuProperty<T>[] as any
|
||||
// 解决如果之前选择的是单规格 spu 的话后面选择多规格 sku 多规格属性信息不展示的问题。解决方法:让 SkuList 组件重新渲染(行折叠会干掉包含的组件展开时会重新加载)
|
||||
setTimeout(() => {
|
||||
expandRowKeys.value = data.map((item) => item.spuId)
|
||||
expandRowKeys.value = data.map((item) => item.spuId + '')
|
||||
}, 200)
|
||||
},
|
||||
{
|
||||
|
@ -116,6 +116,7 @@ import {
|
||||
validityTypeFormat
|
||||
} from '@/views/mall/promotion/coupon/formatter'
|
||||
import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
|
||||
import { CouponTemplateTakeTypeEnum } from '@/utils/constants'
|
||||
|
||||
defineOptions({ name: 'CouponSelect' })
|
||||
|
||||
@ -138,7 +139,7 @@ const queryParams = reactive({
|
||||
pageSize: 10,
|
||||
name: null,
|
||||
discountType: null,
|
||||
canTakeTypes: null
|
||||
canTakeTypes: [CouponTemplateTakeTypeEnum.USER.type] // 只获得直接领取的券
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const selectedCouponList = ref<CouponTemplateApi.CouponTemplateVO[]>([]) // 选择的数据
|
||||
|
@ -16,10 +16,14 @@ export const discountFormat = (row: CouponTemplateVO) => {
|
||||
|
||||
// 格式化【领取上限】
|
||||
export const takeLimitCountFormat = (row: CouponTemplateVO) => {
|
||||
if (row.takeLimitCount === -1) {
|
||||
return '无领取限制'
|
||||
if (row.takeLimitCount) {
|
||||
if (row.takeLimitCount === -1) {
|
||||
return '无领取限制'
|
||||
}
|
||||
return `${row.takeLimitCount} 张/人`
|
||||
} else {
|
||||
return ' '
|
||||
}
|
||||
return `${row.takeLimitCount} 张/人`
|
||||
}
|
||||
|
||||
// 格式化【有效期限】
|
||||
@ -33,8 +37,19 @@ export const validityTypeFormat = (row: CouponTemplateVO) => {
|
||||
return '未知【' + row.validityType + '】'
|
||||
}
|
||||
|
||||
// 格式化【totalCount】
|
||||
export const totalCountFormat = (row: CouponTemplateVO) => {
|
||||
if (row.totalCount === -1) {
|
||||
return '不限制'
|
||||
}
|
||||
return row.totalCount
|
||||
}
|
||||
|
||||
// 格式化【剩余数量】
|
||||
export const remainedCountFormat = (row: CouponTemplateVO) => {
|
||||
if (row.totalCount === -1) {
|
||||
return '不限制'
|
||||
}
|
||||
return row.totalCount - row.takeCount
|
||||
}
|
||||
|
||||
|
@ -115,6 +115,7 @@
|
||||
<el-radio-group v-model="formData.takeType">
|
||||
<el-radio :key="1" :value="1">直接领取</el-radio>
|
||||
<el-radio :key="2" :value="2">指定发放</el-radio>
|
||||
<el-radio :key="2" :value="3">新人卷</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.takeType === 1" label="发放数量" prop="totalCount">
|
||||
@ -309,7 +310,9 @@ const submitForm = async () => {
|
||||
validEndTime:
|
||||
formData.value.validTimes && formData.value.validTimes.length === 2
|
||||
? formData.value.validTimes[1]
|
||||
: undefined
|
||||
: undefined,
|
||||
totalCount: formData.value.takeType === 1 ? formData.value.totalCount : -1,
|
||||
takeLimitCount: formData.value.takeType === 1 ? formData.value.takeLimitCount : -1
|
||||
} as unknown as CouponTemplateApi.CouponTemplateVO
|
||||
|
||||
// 设置商品范围
|
||||
|
@ -109,7 +109,12 @@
|
||||
prop="validityType"
|
||||
width="185"
|
||||
/>
|
||||
<el-table-column align="center" label="发放数量" prop="totalCount" />
|
||||
<el-table-column
|
||||
:formatter="totalCountFormat"
|
||||
align="center"
|
||||
label="发放数量"
|
||||
prop="totalCount"
|
||||
/>
|
||||
<el-table-column
|
||||
:formatter="remainedCountFormat"
|
||||
align="center"
|
||||
@ -189,6 +194,7 @@ import {
|
||||
discountFormat,
|
||||
remainedCountFormat,
|
||||
takeLimitCountFormat,
|
||||
totalCountFormat,
|
||||
validityTypeFormat
|
||||
} from '@/views/mall/promotion/coupon/formatter'
|
||||
|
||||
|
@ -8,28 +8,40 @@
|
||||
:schema="allSchemas.formSchema"
|
||||
>
|
||||
<!-- 先选择 -->
|
||||
<!-- TODO @zhangshuai:商品允许选择多个 -->
|
||||
<!-- TODO @zhangshuai:选择后的 SKU,需要后面加个【删除】按钮 -->
|
||||
<!-- TODO @zhangshuai:展示的金额,貌似不对,大了 100 倍,需要看下 -->
|
||||
<!-- TODO @zhangshuai:“优惠类型”,是每个 SKU 可以自定义已设置哈。因为每个商品 SKU 的折扣和减少价格,可能不同。具体交互,可以注册一个 youzan.com 看看;它的交互方式是,如果设置了“优惠金额”,则算“减价”;如果再次设置了“折扣百分比”,就算“打折”;这样形成一个互斥的优惠类型 -->
|
||||
<template #spuId>
|
||||
<el-button @click="spuSelectRef.open()">选择商品</el-button>
|
||||
<SpuAndSkuList
|
||||
ref="spuAndSkuListRef"
|
||||
:deletable="true"
|
||||
:rule-config="ruleConfig"
|
||||
:spu-list="spuList"
|
||||
:spu-property-list-p="spuPropertyList"
|
||||
:isDelete="true"
|
||||
@delete="deleteSpu"
|
||||
>
|
||||
<el-table-column align="center" label="优惠金额" min-width="168">
|
||||
<template #default="{ row: sku }">
|
||||
<el-input-number v-model="sku.productConfig.discountPrice" :min="0" class="w-100%" />
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.productConfig.discountPrice"
|
||||
:max="parseFloat(fenToYuan(row.price))"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
@change="handleSkuDiscountPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="折扣百分比(%)" min-width="168">
|
||||
<template #default="{ row: sku }">
|
||||
<el-input-number v-model="sku.productConfig.discountPercent" class="w-100%" />
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.productConfig.discountPercent"
|
||||
:max="100"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-100%"
|
||||
@change="handleSkuDiscountPercentChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</SpuAndSkuList>
|
||||
@ -45,11 +57,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { SpuAndSkuList, SpuProperty, SpuSelect } from '../components'
|
||||
import { allSchemas, rules } from './discountActivity.data'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { cloneDeep, debounce } from 'lodash-es'
|
||||
import * as DiscountActivityApi from '@/api/mall/promotion/discount/discountActivity'
|
||||
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||
import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
|
||||
import { formatToFraction } from '@/utils'
|
||||
import { convertToInteger, erpCalculatePercentage, fenToYuan, yuanToFen } from '@/utils'
|
||||
import { PromotionDiscountTypeEnum } from '@/utils/constants'
|
||||
|
||||
defineOptions({ name: 'PromotionDiscountActivityForm' })
|
||||
|
||||
@ -65,7 +78,13 @@ const formRef = ref() // 表单 Ref
|
||||
|
||||
const spuSelectRef = ref() // 商品和属性选择 Ref
|
||||
const spuAndSkuListRef = ref() // sku 限时折扣 配置组件Ref
|
||||
const ruleConfig: RuleConfig[] = []
|
||||
const ruleConfig: RuleConfig[] = [
|
||||
{
|
||||
name: 'productConfig.discountPrice',
|
||||
rule: (arg) => arg > 0,
|
||||
message: '商品优惠金额不能为 0 !!!'
|
||||
}
|
||||
]
|
||||
const spuList = ref<DiscountActivityApi.SpuExtension[]>([]) // 选择的 spu
|
||||
const spuPropertyList = ref<SpuProperty<DiscountActivityApi.SpuExtension>[]>([])
|
||||
const spuIds = ref<number[]>([])
|
||||
@ -101,21 +120,20 @@ const getSpuDetails = async (
|
||||
selectSkus?.forEach((sku) => {
|
||||
let config: DiscountActivityApi.DiscountProductVO = {
|
||||
skuId: sku.id!,
|
||||
spuId: spu.id,
|
||||
spuId: spu.id!,
|
||||
discountType: 1,
|
||||
discountPercent: 0,
|
||||
discountPrice: 0
|
||||
}
|
||||
if (typeof products !== 'undefined') {
|
||||
const product = products.find((item) => item.skuId === sku.id)
|
||||
if (product) {
|
||||
product.discountPercent = fenToYuan(product.discountPercent) as any
|
||||
product.discountPrice = fenToYuan(product.discountPrice) as any
|
||||
}
|
||||
config = product || config
|
||||
}
|
||||
sku.productConfig = config
|
||||
sku.price = formatToFraction(sku.price)
|
||||
sku.marketPrice = formatToFraction(sku.marketPrice)
|
||||
sku.costPrice = formatToFraction(sku.costPrice)
|
||||
sku.firstBrokeragePrice = formatToFraction(sku.firstBrokeragePrice)
|
||||
sku.secondBrokeragePrice = formatToFraction(sku.secondBrokeragePrice)
|
||||
})
|
||||
spu.skus = selectSkus as DiscountActivityApi.SkuExtension[]
|
||||
spuPropertyList.value.push({
|
||||
@ -168,25 +186,13 @@ const submitForm = async () => {
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formRef.value.formModel as DiscountActivityApi.DiscountActivityVO
|
||||
// 获取折扣商品配置
|
||||
const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
|
||||
// 校验优惠金额、折扣百分比,是否正确
|
||||
// TODO @puhui999:这个交互,可以参考下 youzan 的
|
||||
let discountInvalid = false
|
||||
products.forEach((item: DiscountActivityApi.DiscountProductVO) => {
|
||||
if (item.discountPrice != null && item.discountPrice > 0) {
|
||||
item.discountType = 1
|
||||
} else if (item.discountPercent != null && item.discountPercent > 0) {
|
||||
item.discountType = 2
|
||||
} else {
|
||||
discountInvalid = true
|
||||
}
|
||||
item.discountPercent = convertToInteger(item.discountPercent)
|
||||
item.discountPrice = convertToInteger(yuanToFen(item.discountPrice))
|
||||
})
|
||||
if (discountInvalid) {
|
||||
message.error('优惠金额和折扣百分比需要填写一个')
|
||||
return
|
||||
}
|
||||
const data = cloneDeep(formRef.value.formModel) as DiscountActivityApi.DiscountActivityVO
|
||||
data.products = products
|
||||
// 真正提交
|
||||
if (formType.value === 'create') {
|
||||
@ -204,6 +210,36 @@ const submitForm = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理 sku 优惠金额变动 */
|
||||
const handleSkuDiscountPriceChange = debounce((row: any) => {
|
||||
// 校验边界
|
||||
if (row.productConfig.discountPrice <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置优惠类型:满减
|
||||
row.productConfig.discountType = PromotionDiscountTypeEnum.PRICE.type
|
||||
// 设置折扣
|
||||
row.productConfig.discountPercent = erpCalculatePercentage(
|
||||
row.price - yuanToFen(row.productConfig.discountPrice),
|
||||
row.price
|
||||
)
|
||||
}, 200)
|
||||
/** 处理 sku 优惠折扣变动 */
|
||||
const handleSkuDiscountPercentChange = debounce((row: any) => {
|
||||
// 校验边界
|
||||
if (row.productConfig.discountPercent <= 0 || row.productConfig.discountPercent >= 100) {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置优惠类型:折扣
|
||||
row.productConfig.discountType = PromotionDiscountTypeEnum.PERCENT.type
|
||||
// 设置满减金额
|
||||
row.productConfig.discountPrice = fenToYuan(
|
||||
row.price - row.price * (row.productConfig.discountPercent / 100.0 || 0)
|
||||
)
|
||||
}, 200)
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = async () => {
|
||||
spuList.value = []
|
||||
|
@ -1,10 +1,8 @@
|
||||
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
|
||||
import { dateFormatter2 } from '@/utils/formatTime'
|
||||
|
||||
// TODO @zhangshai:
|
||||
// 表单校验
|
||||
export const rules = reactive({
|
||||
spuId: [required],
|
||||
name: [required],
|
||||
startTime: [required],
|
||||
endTime: [required],
|
||||
@ -72,6 +70,17 @@ const crudSchemas = reactive<CrudSchema[]>([
|
||||
width: 120
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '优惠类型',
|
||||
field: 'discountType',
|
||||
dictType: DICT_TYPE.PROMOTION_DISCOUNT_TYPE,
|
||||
dictClass: 'number',
|
||||
isSearch: true,
|
||||
form: {
|
||||
component: 'Radio',
|
||||
value: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '活动商品',
|
||||
field: 'spuId',
|
||||
|
227
src/views/mall/promotion/point/activity/PointActivityForm.vue
Normal file
227
src/views/mall/promotion/point/activity/PointActivityForm.vue
Normal file
@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
|
||||
<Form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:isCol="true"
|
||||
:rules="rules"
|
||||
:schema="allSchemas.formSchema"
|
||||
>
|
||||
<!-- 先选择 -->
|
||||
<template #spuId>
|
||||
<el-button v-if="!isFormUpdate" @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.stock"
|
||||
:max="sku.stock"
|
||||
:min="0"
|
||||
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.count" :min="0" 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.point" :min="0" 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.price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
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" @confirm="selectSpu" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { SpuAndSkuList, SpuProperty, SpuSelect } from '../../components'
|
||||
import { allSchemas, rules } from './pointActivity.data'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import {
|
||||
PointActivityApi,
|
||||
PointActivityVO,
|
||||
PointProductVO,
|
||||
SkuExtension,
|
||||
SpuExtension
|
||||
} from '@/api/mall/promotion/point'
|
||||
import * as ProductSpuApi from '@/api/mall/product/spu'
|
||||
import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
|
||||
import { convertToInteger, formatToFraction } from '@/utils'
|
||||
|
||||
defineOptions({ name: 'PromotionSeckillActivityForm' })
|
||||
|
||||
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 isFormUpdate = ref(false) // 是否更新表单
|
||||
|
||||
// ================= 商品选择相关 =================
|
||||
|
||||
const spuSelectRef = ref() // 商品和属性选择 Ref
|
||||
const spuAndSkuListRef = ref() // sku 积分商城商品配置组件Ref
|
||||
const ruleConfig: RuleConfig[] = [
|
||||
{
|
||||
name: 'productConfig.stock',
|
||||
rule: (arg) => arg >= 1,
|
||||
message: '商品可兑换库存必须大于等于 1 !!!'
|
||||
},
|
||||
{
|
||||
name: 'productConfig.point',
|
||||
rule: (arg) => arg >= 1,
|
||||
message: '商品所需兑换积分必须大于等于 1 !!!'
|
||||
},
|
||||
{
|
||||
name: 'productConfig.count',
|
||||
rule: (arg) => arg >= 1,
|
||||
message: '商品可兑换次数必须大于等于 1 !!!'
|
||||
}
|
||||
]
|
||||
const spuList = ref<SpuExtension[]>([]) // 选择的 spu
|
||||
const spuPropertyList = ref<SpuProperty<SpuExtension>[]>([])
|
||||
const selectSpu = (spuId: number, skuIds: number[]) => {
|
||||
formRef.value.setValues({ spuId })
|
||||
getSpuDetails(spuId, skuIds)
|
||||
}
|
||||
/**
|
||||
* 获取 SPU 详情
|
||||
*/
|
||||
const getSpuDetails = async (
|
||||
spuId: number,
|
||||
skuIds: number[] | undefined,
|
||||
products?: PointProductVO[]
|
||||
) => {
|
||||
const spuProperties: SpuProperty<SpuExtension>[] = []
|
||||
const res = (await ProductSpuApi.getSpuDetailList([spuId])) as 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: PointProductVO = {
|
||||
skuId: sku.id!,
|
||||
stock: 0,
|
||||
price: 0,
|
||||
point: 0,
|
||||
count: 0
|
||||
}
|
||||
if (typeof products !== 'undefined') {
|
||||
const product = products.find((item) => item.skuId === sku.id)
|
||||
if (product) {
|
||||
product.price = formatToFraction(product.price) as any
|
||||
}
|
||||
config = product || config
|
||||
}
|
||||
sku.productConfig = config
|
||||
})
|
||||
spu.skus = selectSkus as 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 PointActivityApi.getPointActivity(id)) as PointActivityVO
|
||||
isFormUpdate.value = true
|
||||
await getSpuDetails(
|
||||
data.spuId!,
|
||||
data.products?.map((sku) => sku.skuId),
|
||||
data.products
|
||||
)
|
||||
formRef.value.setValues(data)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
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 products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
|
||||
products.forEach((item: PointProductVO) => {
|
||||
item.price = convertToInteger(item.price)
|
||||
})
|
||||
const data = formRef.value.formModel as PointActivityVO
|
||||
data.products = products
|
||||
// 真正提交
|
||||
if (formType.value === 'create') {
|
||||
await PointActivityApi.createPointActivity(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await PointActivityApi.updatePointActivity(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = async () => {
|
||||
spuList.value = []
|
||||
spuPropertyList.value = []
|
||||
isFormUpdate.value = false
|
||||
await nextTick()
|
||||
formRef.value.getElFormRef().resetFields()
|
||||
}
|
||||
</script>
|
219
src/views/mall/promotion/point/activity/index.vue
Normal file
219
src/views/mall/promotion/point/activity/index.vue
Normal file
@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<doc-alert title="【营销】积分商城活动" url="https://doc.iocoder.cn/mall/promotion-point/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="活动状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
class="!w-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>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['promotion:point-activity: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" :show-overflow-tooltip="true" :stripe="true">
|
||||
<el-table-column label="活动编号" min-width="80" prop="id" />
|
||||
<el-table-column label="商品图片" min-width="80" prop="spuName">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
:preview-src-list="[scope.row.picUrl]"
|
||||
:src="scope.row.picUrl"
|
||||
class="h-40px w-40px"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品标题" min-width="300" prop="spuName" />
|
||||
<el-table-column
|
||||
:formatter="fenToYuanFormat"
|
||||
label="原价"
|
||||
min-width="100"
|
||||
prop="marketPrice"
|
||||
/>
|
||||
<el-table-column label="原价" min-width="100" prop="marketPrice" />
|
||||
<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 align="center" label="库存" min-width="80" prop="stock" />
|
||||
<el-table-column align="center" label="总库存" min-width="80" prop="totalStock" />
|
||||
<el-table-column align="center" label="已兑换数量" min-width="100" prop="redeemedQuantity">
|
||||
<template #default="{ row }">
|
||||
{{ getRedeemedQuantity(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column align="center" fixed="right" label="操作" width="150px">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['promotion:point-activity:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 0"
|
||||
v-hasPermi="['promotion:point-activity:close']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleClose(scope.row.id)"
|
||||
>
|
||||
关闭
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
v-hasPermi="['promotion:point-activity:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<PointActivityForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import PointActivityForm from './PointActivityForm.vue'
|
||||
import { fenToYuanFormat } from '@/utils/formatter'
|
||||
import { PointActivityApi } from '@/api/mall/promotion/point'
|
||||
|
||||
defineOptions({ name: 'PointActivity' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: null,
|
||||
status: null
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const getRedeemedQuantity = computed(() => (row: any) => (row.totalStock || 0) - (row.stock || 0)) // 获得商品已兑换数量
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await PointActivityApi.getPointActivityPage(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 handleClose = async (id: number) => {
|
||||
try {
|
||||
// 关闭的二次确认
|
||||
await message.confirm('确认关闭该积分商城活动吗?')
|
||||
// 发起关闭
|
||||
await PointActivityApi.closePointActivity(id)
|
||||
message.success('关闭成功')
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await PointActivityApi.deletePointActivity(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
await getList()
|
||||
})
|
||||
</script>
|
@ -0,0 +1,55 @@
|
||||
import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
|
||||
|
||||
// 表单校验
|
||||
export const rules = reactive({
|
||||
spuId: [required],
|
||||
sort: [required]
|
||||
})
|
||||
|
||||
// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
|
||||
const crudSchemas = reactive<CrudSchema[]>([
|
||||
{
|
||||
label: '排序',
|
||||
field: 'sort',
|
||||
form: {
|
||||
component: 'InputNumber',
|
||||
value: 0
|
||||
},
|
||||
table: {
|
||||
width: 80
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '积分商城活动商品',
|
||||
field: 'spuId',
|
||||
isTable: true,
|
||||
isSearch: false,
|
||||
form: {
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
field: 'remark',
|
||||
isSearch: false,
|
||||
form: {
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 4
|
||||
},
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
table: {
|
||||
width: 300
|
||||
}
|
||||
}
|
||||
])
|
||||
export const { allSchemas } = useCrudSchemas(crudSchemas)
|
154
src/views/mall/promotion/point/components/PointShowcase.vue
Normal file
154
src/views/mall/promotion/point/components/PointShowcase.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center gap-8px">
|
||||
<div
|
||||
v-for="(pointActivity, index) in pointActivityList"
|
||||
:key="pointActivity.id"
|
||||
class="select-box spu-pic"
|
||||
>
|
||||
<el-tooltip :content="pointActivity.name">
|
||||
<div class="relative h-full w-full">
|
||||
<el-image :src="pointActivity.picUrl" class="h-full w-full" />
|
||||
<Icon
|
||||
v-show="!disabled"
|
||||
class="del-icon"
|
||||
icon="ep:circle-close-filled"
|
||||
@click="handleRemoveActivity(index)"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tooltip v-if="canAdd" content="选择活动">
|
||||
<div class="select-box" @click="openSeckillActivityTableSelect">
|
||||
<Icon icon="ep:plus" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<!-- 拼团活动选择对话框(表格形式) -->
|
||||
<PointTableSelect
|
||||
ref="pointActivityTableSelectRef"
|
||||
:multiple="limit != 1"
|
||||
@change="handleActivitySelected"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import PointTableSelect from './PointTableSelect.vue'
|
||||
import { PointActivityApi, PointActivityVO } from '@/api/mall/promotion/point'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { oneOfType } from 'vue-types'
|
||||
import { isArray } from '@/utils/is'
|
||||
|
||||
// 活动橱窗,一般用于装修时使用
|
||||
// 提供功能:展示活动列表、添加活动、删除活动
|
||||
defineOptions({ name: 'PointShowcase' })
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: oneOfType<number | Array<number>>([Number, Array]).isRequired,
|
||||
// 限制数量:默认不限制
|
||||
limit: propTypes.number.def(Number.MAX_VALUE),
|
||||
disabled: propTypes.bool.def(false)
|
||||
})
|
||||
|
||||
// 计算是否可以添加
|
||||
const canAdd = computed(() => {
|
||||
// 情况一:禁用时不可以添加
|
||||
if (props.disabled) return false
|
||||
// 情况二:未指定限制数量时,可以添加
|
||||
if (!props.limit) return true
|
||||
// 情况三:检查已添加数量是否小于限制数量
|
||||
return pointActivityList.value.length < props.limit
|
||||
})
|
||||
|
||||
// 拼团活动列表
|
||||
const pointActivityList = ref<PointActivityVO[]>([])
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async () => {
|
||||
const ids = isArray(props.modelValue)
|
||||
? // 情况一:多选
|
||||
props.modelValue
|
||||
: // 情况二:单选
|
||||
props.modelValue
|
||||
? [props.modelValue]
|
||||
: []
|
||||
// 不需要返显
|
||||
if (ids.length === 0) {
|
||||
pointActivityList.value = []
|
||||
return
|
||||
}
|
||||
// 只有活动发生变化之后,才会查询活动
|
||||
if (
|
||||
pointActivityList.value.length === 0 ||
|
||||
pointActivityList.value.some((pointActivity) => !ids.includes(pointActivity.id!))
|
||||
) {
|
||||
pointActivityList.value = await PointActivityApi.getPointActivityListByIds(ids)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/** 活动表格选择对话框 */
|
||||
const pointActivityTableSelectRef = ref()
|
||||
// 打开对话框
|
||||
const openSeckillActivityTableSelect = () => {
|
||||
pointActivityTableSelectRef.value.open(pointActivityList.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择活动后触发
|
||||
* @param activityList 选中的活动列表
|
||||
*/
|
||||
const handleActivitySelected = (activityList: PointActivityVO | PointActivityVO[]) => {
|
||||
pointActivityList.value = isArray(activityList) ? activityList : [activityList]
|
||||
emitActivityChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除活动
|
||||
* @param index 活动索引
|
||||
*/
|
||||
const handleRemoveActivity = (index: number) => {
|
||||
pointActivityList.value.splice(index, 1)
|
||||
emitActivityChange()
|
||||
}
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
const emitActivityChange = () => {
|
||||
if (props.limit === 1) {
|
||||
const pointActivity = pointActivityList.value.length > 0 ? pointActivityList.value[0] : null
|
||||
emit('update:modelValue', pointActivity?.id || 0)
|
||||
emit('change', pointActivity)
|
||||
} else {
|
||||
emit(
|
||||
'update:modelValue',
|
||||
pointActivityList.value.map((pointActivity) => pointActivity.id)
|
||||
)
|
||||
emit('change', pointActivityList.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-box {
|
||||
display: flex;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spu-pic {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.del-icon {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
z-index: 1;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
</style>
|
300
src/views/mall/promotion/point/components/PointTableSelect.vue
Normal file
300
src/views/mall/promotion/point/components/PointTableSelect.vue
Normal file
@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :appendToBody="true" title="选择活动" width="70%">
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="活动状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
class="!w-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>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="loading" :data="list" show-overflow-tooltip>
|
||||
<!-- 1. 多选模式(不能使用type="selection",Element会忽略Header插槽) -->
|
||||
<el-table-column v-if="multiple" width="55">
|
||||
<template #header>
|
||||
<el-checkbox
|
||||
v-model="isCheckAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAll"
|
||||
/>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox
|
||||
v-model="checkedStatus[row.id]"
|
||||
@change="(checked: boolean) => handleCheckOne(checked, row, true)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 2. 单选模式 -->
|
||||
<el-table-column v-else label="#" width="55">
|
||||
<template #default="{ row }">
|
||||
<el-radio
|
||||
v-model="selectedActivityId"
|
||||
:value="row.id"
|
||||
@change="handleSingleSelected(row)"
|
||||
>
|
||||
<!-- 空格不能省略,是为了让单选框不显示label,如果不指定label不会有选中的效果 -->
|
||||
|
||||
</el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动编号" min-width="80" prop="id" />
|
||||
<el-table-column label="商品图片" min-width="80" prop="spuName">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
:preview-src-list="[scope.row.picUrl]"
|
||||
:src="scope.row.picUrl"
|
||||
class="h-40px w-40px"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品标题" min-width="300" prop="spuName" />
|
||||
<el-table-column
|
||||
:formatter="fenToYuanFormat"
|
||||
label="原价"
|
||||
min-width="100"
|
||||
prop="marketPrice"
|
||||
/>
|
||||
<el-table-column label="原价" min-width="100" prop="marketPrice" />
|
||||
<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 align="center" label="库存" min-width="80" prop="stock" />
|
||||
<el-table-column align="center" label="总库存" min-width="80" prop="totalStock" />
|
||||
<el-table-column align="center" label="已兑换数量" min-width="100" prop="redeemedQuantity">
|
||||
<template #default="{ row }">
|
||||
{{ getRedeemedQuantity(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180px"
|
||||
/>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
<template v-if="multiple" #footer>
|
||||
<el-button type="primary" @click="handleEmitChange">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { CHANGE_EVENT } from 'element-plus'
|
||||
import { PointActivityApi, PointActivityVO } from '@/api/mall/promotion/point'
|
||||
import { fenToYuanFormat } from '@/utils/formatter'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
|
||||
/**
|
||||
* 活动表格选择对话框
|
||||
* 1. 单选模式:
|
||||
* 1.1 点击表格左侧的单选框时,结束选择,并关闭对话框
|
||||
* 1.2 再次打开时,保持选中状态
|
||||
* 2. 多选模式:
|
||||
* 2.1 点击表格左侧的多选框时,记录选中的活动
|
||||
* 2.2 切换分页时,保持活动的选中状态
|
||||
* 2.3 点击右下角的确定按钮时,结束选择,关闭对话框
|
||||
* 2.4 再次打开时,保持选中状态
|
||||
*/
|
||||
defineOptions({ name: 'PointTableSelect' })
|
||||
|
||||
defineProps({
|
||||
// 多选模式
|
||||
multiple: propTypes.bool.def(false)
|
||||
})
|
||||
|
||||
// 列表的总页数
|
||||
const total = ref(0)
|
||||
// 列表的数据
|
||||
const list = ref<PointActivityVO[]>([])
|
||||
// 列表的加载中
|
||||
const loading = ref(false)
|
||||
// 弹窗的是否展示
|
||||
const dialogVisible = ref(false)
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: null,
|
||||
status: undefined
|
||||
})
|
||||
const getRedeemedQuantity = computed(() => (row: any) => (row.totalStock || 0) - (row.stock || 0)) // 获得商品已兑换数量
|
||||
/** 打开弹窗 */
|
||||
const open = (pointList?: PointActivityVO[]) => {
|
||||
// 重置
|
||||
checkedActivities.value = []
|
||||
checkedStatus.value = {}
|
||||
isCheckAll.value = false
|
||||
isIndeterminate.value = false
|
||||
|
||||
// 处理已选中
|
||||
if (pointList && pointList.length > 0) {
|
||||
checkedActivities.value = [...pointList]
|
||||
checkedStatus.value = Object.fromEntries(pointList.map((activityVO) => [activityVO.id, true]))
|
||||
}
|
||||
|
||||
dialogVisible.value = true
|
||||
resetQuery()
|
||||
}
|
||||
// 提供 open 方法,用于打开弹窗
|
||||
defineExpose({ open })
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await PointActivityApi.getPointActivityPage(queryParams.value)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
// checkbox绑定undefined会有问题,需要给一个bool值
|
||||
list.value.forEach(
|
||||
(activityVO) =>
|
||||
(checkedStatus.value[activityVO.id] = checkedStatus.value[activityVO.id] || false)
|
||||
)
|
||||
// 计算全选框状态
|
||||
calculateIsCheckAll()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryParams.value = {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: null,
|
||||
status: undefined
|
||||
}
|
||||
getList()
|
||||
}
|
||||
|
||||
// 是否全选
|
||||
const isCheckAll = ref(false)
|
||||
// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
|
||||
const isIndeterminate = ref(false)
|
||||
// 选中的活动
|
||||
const checkedActivities = ref<PointActivityVO[]>([])
|
||||
// 选中状态:key为活动ID,value为是否选中
|
||||
const checkedStatus = ref<Record<string, boolean>>({})
|
||||
|
||||
// 选中的活动 activityId
|
||||
const selectedActivityId = ref()
|
||||
/** 单选中时触发 */
|
||||
const handleSingleSelected = (pointActivityVO: PointActivityVO) => {
|
||||
emits(CHANGE_EVENT, pointActivityVO)
|
||||
// 关闭弹窗
|
||||
dialogVisible.value = false
|
||||
// 记住上次选择的ID
|
||||
selectedActivityId.value = pointActivityVO.id
|
||||
}
|
||||
|
||||
/** 多选完成 */
|
||||
const handleEmitChange = () => {
|
||||
// 关闭弹窗
|
||||
dialogVisible.value = false
|
||||
emits(CHANGE_EVENT, [...checkedActivities.value])
|
||||
}
|
||||
|
||||
/** 确认选择时的触发事件 */
|
||||
const emits = defineEmits<{
|
||||
(e: CHANGE_EVENT, v: PointActivityVO | PointActivityVO[] | any): void
|
||||
}>()
|
||||
|
||||
/** 全选/全不选 */
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
isCheckAll.value = checked
|
||||
isIndeterminate.value = false
|
||||
|
||||
list.value.forEach((pointActivity) => handleCheckOne(checked, pointActivity, false))
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中一行
|
||||
* @param checked 是否选中
|
||||
* @param pointActivity 活动
|
||||
* @param isCalcCheckAll 是否计算全选
|
||||
*/
|
||||
const handleCheckOne = (
|
||||
checked: boolean,
|
||||
pointActivity: PointActivityVO,
|
||||
isCalcCheckAll: boolean
|
||||
) => {
|
||||
if (checked) {
|
||||
checkedActivities.value.push(pointActivity)
|
||||
checkedStatus.value[pointActivity.id] = true
|
||||
} else {
|
||||
const index = findCheckedIndex(pointActivity)
|
||||
if (index > -1) {
|
||||
checkedActivities.value.splice(index, 1)
|
||||
checkedStatus.value[pointActivity.id] = false
|
||||
isCheckAll.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 计算全选框状态
|
||||
if (isCalcCheckAll) {
|
||||
calculateIsCheckAll()
|
||||
}
|
||||
}
|
||||
|
||||
// 查找活动在已选中活动列表中的索引
|
||||
const findCheckedIndex = (activityVO: PointActivityVO) =>
|
||||
checkedActivities.value.findIndex((item) => item.id === activityVO.id)
|
||||
|
||||
// 计算全选框状态
|
||||
const calculateIsCheckAll = () => {
|
||||
isCheckAll.value = list.value.every((activityVO) => checkedStatus.value[activityVO.id])
|
||||
// 计算中间状态:不是全部选中 && 任意一个选中
|
||||
isIndeterminate.value =
|
||||
!isCheckAll.value && list.value.some((activityVO) => checkedStatus.value[activityVO.id])
|
||||
}
|
||||
</script>
|
@ -56,7 +56,7 @@
|
||||
label="分类"
|
||||
prop="productCategoryIds"
|
||||
>
|
||||
<ProductCategorySelect v-model="formData.productCategoryIds" />
|
||||
<ProductCategorySelect v-model="formData.productCategoryIds" :multiple="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" placeholder="请输入备注" />
|
||||
@ -119,6 +119,9 @@ const open = async (type: string, id?: number) => {
|
||||
// 规则分转元
|
||||
data.rules?.forEach((item: any) => {
|
||||
item.discountPrice = fenToYuan(item.discountPrice || 0)
|
||||
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
||||
item.limit = fenToYuan(item.limit || 0)
|
||||
}
|
||||
})
|
||||
formData.value = data
|
||||
// 获得商品范围
|
||||
@ -151,6 +154,9 @@ const submitForm = async () => {
|
||||
// 规则元转分
|
||||
data.rules.forEach((item) => {
|
||||
item.discountPrice = yuanToFen(item.discountPrice || 0)
|
||||
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
||||
item.limit = yuanToFen(item.limit || 0)
|
||||
}
|
||||
})
|
||||
// 设置商品范围
|
||||
setProductScopeValues(data)
|
||||
@ -188,7 +194,7 @@ const getProductScope = async () => {
|
||||
case PromotionProductScopeEnum.CATEGORY.scope:
|
||||
await nextTick()
|
||||
let productCategoryIds = formData.value.productScopeValues as any
|
||||
if (Array.isArray(productCategoryIds) && productCategoryIds.length > 0) {
|
||||
if (Array.isArray(productCategoryIds) && productCategoryIds.length === 1) {
|
||||
// 单选时使用数组不能反显
|
||||
productCategoryIds = productCategoryIds[0]
|
||||
}
|
||||
|
@ -10,14 +10,25 @@
|
||||
<el-form ref="formRef" :model="rule">
|
||||
<el-form-item label="优惠门槛:" label-width="100px" prop="limit">
|
||||
满
|
||||
<el-input-number
|
||||
v-if="PromotionConditionTypeEnum.PRICE.type === formData.conditionType"
|
||||
v-model="rule.limit"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.1"
|
||||
class="w-150px! p-x-20px!"
|
||||
placeholder=""
|
||||
type="number"
|
||||
controls-position="right"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="rule.limit"
|
||||
:min="0"
|
||||
class="w-150px! p-x-20px!"
|
||||
placeholder=""
|
||||
type="number"
|
||||
/>
|
||||
<!-- TODO @puhui999:走字典数据? -->
|
||||
{{ PromotionConditionTypeEnum.PRICE.type === formData.conditionType ? '元' : '件' }}
|
||||
</el-form-item>
|
||||
<el-form-item label="优惠内容:" label-width="100px">
|
||||
|
@ -27,7 +27,7 @@
|
||||
placeholder="请选择活动状态"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_ACTIVITY_STATUS)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
@ -55,7 +55,7 @@
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['product:brand:create']"
|
||||
v-hasPermi="['promotion:reward-activity:create']"
|
||||
plain
|
||||
type="primary"
|
||||
@click="openForm('create')"
|
||||
@ -71,6 +71,11 @@
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" default-expand-all row-key="id">
|
||||
<el-table-column label="活动名称" prop="name" />
|
||||
<el-table-column label="活动范围" prop="productScope" >
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE" :value="scope.row.productScope" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
@ -85,7 +90,7 @@
|
||||
/>
|
||||
<el-table-column align="center" label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.PROMOTION_ACTIVITY_STATUS" :value="scope.row.status" />
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@ -98,7 +103,7 @@
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['product:brand:update']"
|
||||
v-hasPermi="['promotion:reward-activity:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
@ -106,7 +111,16 @@
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['product:brand:delete']"
|
||||
v-if="scope.row.status === 0"
|
||||
v-hasPermi="['promotion:reward-activity:close']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleClose(scope.row.id)"
|
||||
>
|
||||
关闭
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['promotion:reward-activity:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
@ -180,6 +194,19 @@ const openForm = (type: string, id?: number) => {
|
||||
formRef.value?.open(type, id)
|
||||
}
|
||||
|
||||
/** 关闭按钮操作 */
|
||||
const handleClose = async (id: number) => {
|
||||
try {
|
||||
// 关闭的二次确认
|
||||
await message.confirm('确认关闭该满减活动吗?')
|
||||
// 发起关闭
|
||||
await RewardActivityApi.closeRewardActivity(id)
|
||||
message.success('关闭成功')
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
|
@ -6,6 +6,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as MpAccountApi from '@/api/mp/account'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
const { push, currentRoute } = useRouter() // 路由
|
||||
|
||||
defineOptions({ name: 'WxAccountSelect' })
|
||||
|
||||
@ -22,6 +27,12 @@ const emit = defineEmits<{
|
||||
|
||||
const handleQuery = async () => {
|
||||
accountList.value = await MpAccountApi.getSimpleAccountList()
|
||||
if (accountList.value.length == 0) {
|
||||
message.error('未配置公众号,请在【公众号管理 -> 账号管理】菜单,进行配置')
|
||||
delView(unref(currentRoute))
|
||||
await push({ name: 'MpAccount' })
|
||||
return
|
||||
}
|
||||
// 默认选中第一个
|
||||
if (accountList.value.length > 0) {
|
||||
account.id = accountList.value[0].id
|
||||
|
@ -3,14 +3,7 @@
|
||||
<ContentWrap>
|
||||
<el-form class="-mb-15px" ref="queryForm" :inline="true" label-width="68px">
|
||||
<el-form-item label="公众号" prop="accountId">
|
||||
<el-select v-model="accountId" @change="getSummary" class="!w-240px">
|
||||
<el-option
|
||||
v-for="item in accountList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<WxAccountSelect @change="onAccountChanged" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围" prop="dateRange">
|
||||
<el-date-picker
|
||||
@ -76,7 +69,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { formatDate, addTime, betweenDay, beginOfDay, endOfDay } from '@/utils/formatTime'
|
||||
import * as StatisticsApi from '@/api/mp/statistics'
|
||||
import * as MpAccountApi from '@/api/mp/account'
|
||||
import WxAccountSelect from '@/views/mp/components/wx-account-select'
|
||||
|
||||
defineOptions({ name: 'MpStatistics' })
|
||||
|
||||
@ -88,7 +81,6 @@ const dateRange = ref([
|
||||
endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24))
|
||||
])
|
||||
const accountId = ref(-1) // 选中的公众号编号
|
||||
const accountList = ref<MpAccountApi.AccountVO[]>([]) // 公众号账号列表
|
||||
|
||||
const xAxisDate = ref([] as any[]) // X 轴的日期范围
|
||||
// 用户增减数据图表配置项
|
||||
@ -230,13 +222,10 @@ const interfaceSummaryOption = reactive({
|
||||
]
|
||||
})
|
||||
|
||||
/** 加载公众号账号的列表 */
|
||||
const getAccountList = async () => {
|
||||
accountList.value = await MpAccountApi.getSimpleAccountList()
|
||||
// 默认选中第一个
|
||||
if (accountList.value.length > 0) {
|
||||
accountId.value = accountList.value[0].id!
|
||||
}
|
||||
/** 侦听公众号变化 **/
|
||||
const onAccountChanged = (id: number) => {
|
||||
accountId.value = id
|
||||
getSummary()
|
||||
}
|
||||
|
||||
/** 加载数据 */
|
||||
@ -357,12 +346,4 @@ const interfaceSummaryChart = async () => {
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取公众号下拉列表
|
||||
await getAccountList()
|
||||
// 加载数据
|
||||
getSummary()
|
||||
})
|
||||
</script>
|
||||
|
@ -231,7 +231,7 @@ const getDetail = async () => {
|
||||
goReturnUrl('cancel')
|
||||
return
|
||||
}
|
||||
const data = await PayOrderApi.getOrder(id.value)
|
||||
const data = await PayOrderApi.getOrder(id.value, true)
|
||||
payOrder.value = data
|
||||
// 1.2 无法查询到支付信息
|
||||
if (!data) {
|
||||
|
Reference in New Issue
Block a user