Commit 9d61862a by yuzhenWang

Merge branch 'feature-20251125wyz-做产品上架' into 'dev'

Feature 20251125wyz 做产品上架

See merge request !6
parents 3c82c9ed 710c08f3
......@@ -5,4 +5,4 @@ VITE_APP_TITLE = 银盾中台系统
VITE_APP_ENV = 'development'
# 若依管理系统/开发环境
VITE_APP_BASE_API = 'http://10.0.10.26:9002'
VITE_APP_BASE_API = 'http://139.224.145.34:9002'
# 使用 Node.js 18 基础镜像 (Debian bullseye)
FROM docker.m.daocloud.io/library/node:18-bullseye AS build
# 设置环境变量
ENV npm_config_canvas_binary_host_mirror="https://npmmirror.com/mirrors/canvas"
ENV npm_config_sharp_binary_host="https://npmmirror.com/mirrors/sharp"
ENV npm_config_sharp_libvips_binary_host="https://npmmirror.com/mirrors/sharp-libvips"
# 添加 esbuild 镜像源
ENV ESBUILD_BINARY_HOST="https://npmmirror.com/mirrors/esbuild"
# 安装系统依赖 (使用阿里云镜像源)
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
apt-get update && apt-get install -y --no-install-recommends \
build-essential \
python3 \
pkg-config \
libcairo2-dev \
libpango1.0-dev \
libjpeg-dev \
libgif-dev \
librsvg2-dev \
libvips-dev \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 复制包管理文件
COPY package.json ./
# 1. 显式安装 esbuild 二进制
RUN npm install @esbuild/linux-x64 --verbose --registry=https://registry.npmmirror.com
# 2. 安装其他原生模块
RUN npm install canvas@2.11.2 --verbose --ignore-scripts
RUN npm install sharp@0.32.6 --verbose --ignore-scripts
# 3. 安装项目依赖(移除 --no-optional)
RUN npm install --verbose --registry=https://registry.npmmirror.com
# 4. 验证 esbuild 安装
RUN ls -la node_modules/esbuild/bin && \
./node_modules/.bin/esbuild --version
# 复制源码并构建
COPY . .
RUN npm run build:dev
# 生产阶段
FROM docker.m.daocloud.io/library/nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 设置时区
RUN echo "https://mirrors.aliyun.com/alpine/v3.22/main/" > /etc/apk/repositories && \
echo "https://mirrors.aliyun.com/alpine/v3.22/community/" >> /etc/apk/repositories && \
apk update && \
apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
EXPOSE 6699
CMD ["nginx", "-g", "daemon off;"]
server {
listen 6699;
server_name 139.224.145.34;
# 处理前端静态资源(Vue应用)
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html; # 处理Vue路由history模式
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
limit_except GET POST PUT DELETE OPTIONS {
deny all;
}
}
}
......@@ -7,9 +7,10 @@
"type": "module",
"scripts": {
"dev": "vite",
"build:prod": "vite build",
"build:stage": "vite build --mode staging",
"preview": "vite preview"
"build": "vite build",
"build:dev": "vite build --mode=development",
"preview": "vite preview",
"report": "npm run build --report"
},
"repository": {
"type": "git",
......@@ -28,6 +29,7 @@
"js-beautify": "1.14.11",
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
"lodash-es": "^4.17.21",
"nprogress": "0.2.0",
"pinia": "3.0.2",
"splitpanes": "^4.0.4",
......
......@@ -20,9 +20,17 @@ export function uploadImage(data) {
})
}
// 查询用户是否有访问项目的权限
export function getVisitPermission(projectBizId) {
export function getVisitPermission(projectBizId, tenantBizId) {
return request({
url: '/user/api/sysUser/login/permission/project/visit?projectBizId=' + projectBizId,
url: `/user/api/sysUser/login/permission/project/visit?projectBizId=${projectBizId}&tenantBizId=${tenantBizId}`,
method: 'get'
})
}
//获取保险公司列表
export function getAllCompanys(data) {
return request({
url: '/user/api/sysDept/company/page',
method: 'post',
data: data
})
}
import request from '@/utils/request'
// 查询产品上架列表
export function getProductList(data) {
return request({
url: '/product/api/product/page',
method: 'post',
data: data
})
}
// 查询产品上架列表
export function getCategoryTreeList(data) {
return request({
url: '/base/api/category/page',
method: 'post',
data: data
})
}
// 新增产品
export function addProduct(data) {
return request({
url: '/product/api/productLaunch/add/category',
method: 'post',
data: data
})
}
// 获取产品详情
export function getProductDetail(productLaunchBizId) {
return request({
url: `/product/api/productLaunch/detail?productLaunchBizId=${productLaunchBizId}`,
method: 'get'
})
}
// 查询产品参数信息
export function productParams(data) {
return request({
url: '/base/api/relObjectField/query',
method: 'post',
data: data
})
}
// 查询产品参数右侧下拉框数据
export function ParamRightOptions(data) {
return request({
url: '/base/api/relFieldValue/field/list',
method: 'post',
data: data
})
}
// 查询产品规格信息列表
export function productSpecies(data) {
return request({
url: '/base/api/relObjectSpecies/query',
method: 'post',
data: data
})
}
// 查询选中的分类列表
export function querySelectCategory(data) {
return request({
url: '/base/api/relObjectCategory/query/selected',
method: 'post',
data: data
})
}
// 保存上架产品
export function saveProductLaunch(data) {
return request({
url: '/product/api/productLaunch/save',
method: 'post',
data: data
})
}
// 发佣管理更新规格信息
export function exportSpecies(data) {
return request({
url: '/product/api/announcementSpecies/import/species',
method: 'post',
data: data
})
}
// 发佣管理查询公告佣列表
export function sendSpecies(data) {
return request({
url: '/product/api/announcementSpecies/page',
method: 'post',
data: data
})
}
// 发佣管理查询公告佣金明细列表
export function sendCommissionRatio(data) {
return request({
url: '/product/api/announcementCommissionRatio/page',
method: 'post',
data: data
})
}
// 发佣管理批量保存公告佣金设置
export function saveBatchSendCommission(data) {
return request({
url: '/product/api/announcementCommissionRatio/batch/save',
method: 'post',
data: data
})
}
// 发佣管理单个删除公告佣金设置
export function deleteCommission(announcementCommissionRatioBizId) {
return request({
url: `/product/api/announcementCommissionRatio/del?announcementCommissionRatioBizId=${announcementCommissionRatioBizId}`,
method: 'delete'
})
}
// 获得来佣列表的数据
export function comeCommissionList(data) {
return request({
url: '/product/api/expectedSpecies/page',
method: 'post',
data: data
})
}
// 获得来佣列表的数据
export function comeExpectedSpecies(data) {
return request({
url: '/product/api/expectedSpecies/import/species',
method: 'post',
data: data
})
}
// 批量保存来佣佣金设置
export function comeBatchSave(data) {
return request({
url: '/product/api/expectedCommissionRatio/batch/save',
method: 'post',
data: data
})
}
// 批量保存来佣佣金设置
export function comeCommissionRatio(data) {
return request({
url: '/product/api/expectedCommissionRatio/page',
method: 'post',
data: data
})
}
// 来佣管理单个删除佣金设置
export function deleteComeCommission(expectedCommissionRatioBizId) {
return request({
url: `/product/api/expectedCommissionRatio/del?expectedCommissionRatioBizId=${expectedCommissionRatioBizId}`,
method: 'delete'
})
}
// 商品列表
export function goodsList(data) {
return request({
url: '/product/api/productLaunch/page',
method: 'post',
data: data
})
}
// 审核商品状态
export function productApproval(data) {
return request({
url: '/product/api/productLaunch/approval',
method: 'put',
data: data
})
}
// 修改商品上架下架状态
export function changeProductStatus(data) {
return request({
url: '/product/api/productLaunch/edit/status',
method: 'put',
data: data
})
}
// 修改来佣佣金设置状态
export function changeComeStatus(data) {
return request({
url: `/product/api/expectedCommissionRatio/edit/status?expectedCommissionRatioBizId=${data.expectedCommissionRatioBizId}&status=${data.status}`,
method: 'put'
})
}
// 修改发佣佣金设置状态
export function changeSendStatus(data) {
return request({
url: `/product/api/announcementCommissionRatio/edit/status?announcementCommissionRatioBizId=${data.announcementCommissionRatioBizId}&status=${data.status}`,
method: 'put'
})
}
<template>
<div class="box">
<el-row>
<el-col :span="12">
<!-- v-model="queryParams.productName"
@blur="handleSelectChange"
:remote-method="searchProduct"
-->
<!-- <el-select
filterable
remote
default-first-option
:reserve-keyword="false"
placeholder="类目搜索"
clearable
remote-show-suffix
style="margin-bottom: 10px"
>
<el-option
v-for="item in productList"
:key="item.productBizId"
:label="item.productName"
:value="item.productBizId"
/>
</el-select> -->
</el-col>
<el-col :span="24" v-if="categoryName">
<div class="chooseName">
<span style="color: rgb(0 0 0 / 55%)">已选类目:</span>{{ categoryName }}
</div>
</el-col>
</el-row>
<div class="category-container">
<div class="tableHeader">
<div
class="headerItem"
:style="{ width: `${100 / categoryList.length}%` }"
v-for="item in categoryList"
:key="item.level"
>
{{ item.title }}
</div>
</div>
<div class="tableBody">
<div
class="bodyItem"
v-for="(item, index) in categoryList"
:key="item.level"
:style="{
width: `${100 / categoryList.length}%`,
borderRight: categoryList.length - 1 === index ? 'none' : '1px solid #ccc'
}"
>
<div
class="categoryItem"
v-for="(cItem, cIndex) in item.data"
:key="cItem.id"
@click="handleClickCategory(item, cItem, cIndex)"
:style="{
backgroundColor: cItem.isSelected ? 'rgb(64, 158, 255,0.1)' : null
}"
>
<span> {{ cItem.name }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { getCategoryTreeList } from '@/api/product/index'
import { computed } from 'vue'
import { ref } from 'vue'
const props = defineProps({
// 类目数据,
data: {
type: Array,
default: () => []
},
// 类型,是新增还是编辑,
type: {
type: String,
default: 'add'
}
})
const categoryList = ref([])
const level1Obj = ref([])
// 中文数字单位
const units = ['', '十', '百', '千']
const bigUnits = ['', '万', '亿', '兆', '京', '垓', '秭', '穰', '沟', '涧', '正', '载']
// 中文数字字符
const digits = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
const categoryName = computed(() => {
let result = []
categoryList.value.forEach(item => {
item.data.forEach(c => {
if (c.isSelected) result.push(c.name)
})
})
if (result.length > 0) {
return result.join(' > ')
} else {
return ''
}
})
const emit = defineEmits(['handleClickCategory'])
const handleClickCategory = (fItem, cItem, cIndex) => {
categoryList.value.forEach(item => {
item.data.forEach(c => {
if (c.categoryBizId == cItem.categoryBizId && fItem.level == item.level) {
c.isSelected = true
} else if (fItem.level == item.level) {
c.isSelected = false
}
})
})
getCategoryData(cItem)
}
function getCategoryData(obj) {
// let name = query ? query.trim() : ''
let data = {
// name: name,
type: 'PRODUCT',
pid: obj.categoryBizId ? obj.categoryBizId : '0',
pageSize: 9999,
pageNo: 1
}
getCategoryTreeList(data).then(response => {
if (response.code === 200) {
let res = response.data.records
res.forEach(item => {
item.isSelected = false
})
if (!obj.level) {
categoryList.value = [
{ level: 1, data: res, title: `${convertSection(1)}级类目` },
{ level: 2, data: [], title: `${convertSection(2)}级类目` },
{ level: 3, data: [], title: `${convertSection(3)}级类目` },
{ level: 4, data: [], title: `${convertSection(4)}级类目` }
]
} else {
if (res.length > 0) {
// 先删除当前层级以后的数据
if (categoryList.value.length > 4 && obj.level > 4) {
categoryList.value.splice(obj.level, categoryList.value.length - obj.level)
}
let cIndex = categoryList.value.findIndex(item => item.level == res[0].level)
if (cIndex > -1) {
// 固定显示4个类目,如果超过4个类目,则删除多余的类目
if (categoryList.value.length > 4 && obj.level < 4) {
categoryList.value.splice(4, categoryList.value.length - 4)
}
categoryList.value[cIndex].data = res
for (let i = cIndex + 1; i < categoryList.value.length; i++) {
categoryList.value[i].data = []
}
} else {
categoryList.value.push({
level: res[0].level,
data: res,
title: `${convertSection(res[0].level)}级类目`
})
}
}
}
console.log('categoryList', categoryList.value)
} else {
categoryList.value = []
proxy.$modal.msgError(response.msg)
}
})
}
// 转换4位数字节
const convertSection = sectionNum => {
const len = sectionNum
let result = ''
let hasZero = false
for (let i = 0; i < len; i++) {
const digit = sectionNum
if (digit === 0) {
hasZero = true
} else {
if (hasZero) {
result += '零'
hasZero = false
}
result += digits[digit]
return result
}
}
// 处理全零的情况
if (result === '') {
return '零'
}
return result
}
if (props.type == 'add') {
getCategoryData({ categoryBizId: '0' })
} else if (props.type == 'edit') {
console.log('props.data', props.data);
// 先回显已经选择好的类目
categoryList.value = JSON.parse(JSON.stringify(props.data))
categoryList.value.forEach(item => {
item.title = `${convertSection(item.level)}级类目`
// 还应该把data里的数据都加上 isSelected: true
})
}
// 暴露给父组件
defineExpose({
categoryList,
categoryName
})
</script>
<style lang="scss" scoped>
.box {
width: 100%;
box-sizing: border-box;
}
.category-container {
width: 100%;
border: 1px solid #ccc;
border-radius: 10px;
overflow: hidden;
height: 100%;
}
.tableHeader {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #ccc;
color: rgb(0 0 0 / 55%);
font-size: 16px;
}
.headerItem {
padding: 10px 20px;
text-align: left;
}
.tableBody {
display: flex;
justify-content: space-between;
color: rgb(0 0 0 / 55%);
font-size: 14px;
height: calc(100% - 40px);
}
.bodyItem {
overflow-y: auto;
padding: 10px 10px;
}
.categoryItem {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
.chooseName {
padding: 10px 20px;
background-color: #f7f7f7;
border-radius: 5px;
font-size: 14px;
margin-bottom: 10px;
}
</style>
<template>
<div class="component-upload-image">
<div class="component-upload-media">
<el-upload
multiple
:disabled="disabled"
:action="uploadImgUrl"
list-type="picture-card"
:action="uploadUrl"
:list-type="listType"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
ref="imageUpload"
ref="mediaUpload"
:before-remove="handleDelete"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:on-preview="handlePreview"
:class="{ hide: fileList.length >= limit }"
>
<el-icon class="avatar-uploader-icon"><plus /></el-icon>
......@@ -24,8 +24,14 @@
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
<template v-if="imageSize || videoSize">
<span v-if="imageSize"
>图片大小不超过 <b style="color: #f56c6c">{{ imageSize }}MB</b></span
>
<span v-if="imageSize && videoSize"></span>
<span v-if="videoSize"
>视频大小不超过 <b style="color: #f56c6c">{{ videoSize }}MB</b></span
>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
......@@ -33,8 +39,21 @@
的文件
</div>
<el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
<img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
<!-- 预览对话框 -->
<el-dialog v-model="dialogVisible" :title="previewTitle" width="800px" append-to-body>
<img
v-if="previewType === 'image'"
:src="dialogImageUrl"
style="display: block; max-width: 100%; margin: 0 auto"
/>
<video
v-else-if="previewType === 'video'"
:src="dialogImageUrl"
controls
style="display: block; max-width: 100%; margin: 0 auto"
>
您的浏览器不支持视频播放
</video>
</el-dialog>
</div>
</template>
......@@ -43,6 +62,7 @@
import { getToken } from '@/utils/auth'
import { isExternal } from '@/utils/validate'
import Sortable from 'sortablejs'
import videoThumbnail from '@/assets/images/video-thumbnail.png'
const props = defineProps({
modelValue: [String, Object, Array],
......@@ -51,6 +71,11 @@ const props = defineProps({
type: String,
default: '/common/upload'
},
// 来源
source: {
type: String,
default: ''
},
// 上传携带的参数
data: {
type: Object
......@@ -60,12 +85,17 @@ const props = defineProps({
type: Number,
default: 5
},
// 大小限制(MB)
fileSize: {
// 图片大小限制(MB)
imageSize: {
type: Number,
default: 5
default: 10
},
// 视频大小限制(MB)
videoSize: {
type: Number,
default: 500
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
// 文件类型, 例如['png', 'jpg', 'jpeg', 'mp4']
fileType: {
type: Array,
default: () => ['png', 'jpg', 'jpeg']
......@@ -75,7 +105,7 @@ const props = defineProps({
type: Boolean,
default: true
},
// 禁用组件(仅查看图片
// 禁用组件(仅查看文件
disabled: {
type: Boolean,
default: false
......@@ -84,20 +114,75 @@ const props = defineProps({
drag: {
type: Boolean,
default: true
},
// 列表类型,根据文件类型自动调整
listType: {
type: String,
default: 'picture-card'
}
})
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const emit = defineEmits(['file-change', 'update:modelValue'])
const number = ref(0)
const uploadList = ref([])
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const previewType = ref('image') // 'image' 或 'video'
const previewTitle = ref('预览')
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传的图片服务器地址
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action)
const headers = ref({ Authorization: 'Bearer ' + getToken() })
const fileList = ref([])
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize))
const showTip = computed(
() => props.isShowTip && (props.fileType || props.imageSize || props.videoSize)
)
// 获取文件扩展名
const getFileExtension = fileName => {
if (!fileName) return ''
const lastDotIndex = fileName.lastIndexOf('.')
return lastDotIndex > -1 ? fileName.slice(lastDotIndex + 1).toLowerCase() : ''
}
// 判断文件类型 - 更安全的方式
const isImage = file => {
if (!file) return false
// 优先检查文件类型
if (file.type && typeof file.type === 'string' && file.type.startsWith('image/')) {
return true
}
// 通过文件扩展名判断
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']
const fileExtension = getFileExtension(file.name || file.url || '')
return imageExtensions.includes(fileExtension)
}
const isVideo = file => {
if (!file) return false
// 优先检查文件类型
if (file.type && typeof file.type === 'string' && file.type.startsWith('video/')) {
return true
}
// 通过文件扩展名判断
const videoExtensions = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v', 'webm']
const fileExtension = getFileExtension(file.name || file.url || '')
return videoExtensions.includes(fileExtension)
}
// 获取文件对应的最大大小
const getFileSizeLimit = file => {
if (isImage(file)) {
return props.imageSize
} else if (isVideo(file)) {
return props.videoSize
}
return Math.max(props.imageSize, props.videoSize) // 默认取较大的值
}
watch(
() => props.modelValue,
......@@ -126,37 +211,43 @@ watch(
// 上传前loading加载
function handleBeforeUpload(file) {
let isImg = false
let isFileTypeValid = false
if (props.fileType.length) {
let fileExtension = ''
if (file.name.lastIndexOf('.') > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
}
isImg = props.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true
const fileExtension = getFileExtension(file.name)
isFileTypeValid = props.fileType.some(type => {
if (file.type && file.type.indexOf(type) > -1) return true
if (fileExtension && fileExtension.indexOf(type) > -1) return true
return false
})
} else {
isImg = file.type.indexOf('image') > -1
// 如果没有指定文件类型,默认允许图片和视频
isFileTypeValid =
file.type && (file.type.indexOf('image') > -1 || file.type.indexOf('video') > -1)
}
if (!isImg) {
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join('/')}图片格式文件!`)
if (!isFileTypeValid) {
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join('/')}格式文件!`)
return false
}
if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
return false
}
// 根据文件类型获取对应的大小限制
const maxSize = getFileSizeLimit(file)
const isLt = file.size / 1024 / 1024 < maxSize
if (!isLt) {
const fileType = isImage(file) ? '图片' : '视频'
proxy.$modal.msgError(`上传${fileType}大小不能超过 ${maxSize} MB!`)
return false
}
proxy.$modal.loading('正在上传图片,请稍候...')
proxy.$modal.loading('正在上传文件,请稍候...')
number.value++
return true
}
// 文件个数超出
......@@ -165,27 +256,66 @@ function handleExceed() {
}
// 上传成功回调
// function handleUploadSuccess(res, file) {
// if (res.code === 200) {
// console.log('文件上传成功', res)
// // 添加上传文件的类型信息
// const fileItem = {
// name: res.data.originalName,
// url: res.data.url,
// raw: file // 保存原始文件对象以便后续使用
// }
// uploadList.value.push(fileItem)
// uploadedSuccessfully()
// } else {
// number.value--
// proxy.$modal.closeLoading()
// proxy.$modal.msgError(res.msg)
// proxy.$refs.mediaUpload.handleRemove(file)
// uploadedSuccessfully()
// }
// }
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
console.log('图片上传', res)
console.log('文件上传成功', res)
// 添加上传文件的类型信息
const fileItem = {
name: res.data.originalName,
url: res.data.url,
raw: file, // 保存原始文件对象以便后续使用
isVideo: isVideo(file) // 标记是否为视频
}
// 为视频文件设置一个默认的封面图标
if (isVideo(file)) {
fileItem.thumbnail = videoThumbnail // 准备一个视频封面图标
}
uploadList.value.push({ name: res.data.originalName, url: res.data.url })
uploadList.value.push(fileItem)
uploadedSuccessfully()
} else {
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
proxy.$refs.imageUpload.handleRemove(file)
proxy.$refs.mediaUpload.handleRemove(file)
uploadedSuccessfully()
}
}
// 删除图片
// 删除文件
function handleDelete(file) {
const findex = fileList.value.map(f => f.name).indexOf(file.name)
if (findex > -1 && uploadList.value.length === number.value) {
fileList.value.splice(findex, 1)
emit('update:modelValue', listToString(fileList.value))
const newValue = listToString(fileList.value)
emit('update:modelValue', newValue)
emit('file-change', {
count: fileList.value.length,
fileList: fileList.value,
value: newValue,
type: 'delete',
source: props.source
})
return false
}
}
......@@ -196,20 +326,68 @@ function uploadedSuccessfully() {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
uploadList.value = []
number.value = 0
emit('update:modelValue', listToString(fileList.value))
const newValue = listToString(fileList.value)
emit('update:modelValue', newValue)
proxy.$modal.closeLoading()
emit('file-change', {
count: fileList.value.length,
fileList: fileList.value,
value: newValue,
type: 'success',
source: props.source
})
}
}
// 上传失败
function handleUploadError() {
proxy.$modal.msgError('上传图片失败')
proxy.$modal.msgError('上传文件失败')
proxy.$modal.closeLoading()
}
// 预览
function handlePictureCardPreview(file) {
// 预览 - 修复错误处理
function handlePreview(file) {
console.log('预览文件:', file) // 调试用
if (!file || !file.url) {
proxy.$modal.msgError('文件信息不完整,无法预览')
return
}
dialogImageUrl.value = file.url
// 安全地判断文件类型
try {
if (isImage(file)) {
previewType.value = 'image'
previewTitle.value = '图片预览'
} else if (isVideo(file)) {
previewType.value = 'video'
previewTitle.value = '视频预览'
} else {
// 如果无法确定类型,根据URL扩展名再次判断
const ext = getFileExtension(file.url)
const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']
const videoExts = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v', 'webm']
if (imageExts.includes(ext)) {
previewType.value = 'image'
previewTitle.value = '图片预览'
} else if (videoExts.includes(ext)) {
previewType.value = 'video'
previewTitle.value = '视频预览'
} else {
previewType.value = 'image'
previewTitle.value = '文件预览'
}
}
} catch (error) {
console.error('预览类型判断错误:', error)
// 出错时默认按图片处理
previewType.value = 'image'
previewTitle.value = '文件预览'
}
dialogVisible.value = true
}
......@@ -229,14 +407,17 @@ function listToString(list, separator) {
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
Sortable.create(element, {
onEnd: evt => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
const element = proxy.$refs.mediaUpload?.$el?.querySelector('.el-upload-list')
if (element) {
Sortable.create(element, {
onEnd: evt => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
const newValue = listToString(fileList.value)
emit('update:modelValue', newValue)
}
})
}
})
}
})
......@@ -251,4 +432,45 @@ onMounted(() => {
:deep(.el-upload.el-upload--picture-card.is-disabled) {
display: none !important;
}
.component-upload-media {
:deep(.el-upload-list__item) {
transition: none !important;
// 为视频文件添加特殊样式
&.is-video {
.el-upload-list__item-thumbnail {
position: relative;
&::before {
content: '▶';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: white;
text-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
z-index: 1;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 0;
}
}
}
}
// 视频缩略图样式
:deep(.el-upload-list__item-thumbnail) {
object-fit: cover;
}
}
</style>
......@@ -2,9 +2,11 @@
<section class="app-main">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="tagsViewStore.cachedViews">
<component v-if="!route.meta.link" :is="Component" :key="route.path"/>
</keep-alive>
<!-- 慎用keep-alive缓存标签 -->
<!-- <keep-alive :include="tagsViewStore.cachedViews">
</keep-alive> -->
<component v-if="!route.meta.link" :is="Component" :key="route.path" />
</transition>
</router-view>
<iframe-toggle />
......@@ -13,8 +15,8 @@
</template>
<script setup>
import copyright from "./Copyright/index"
import iframeToggle from "./IframeToggle/index"
import copyright from './Copyright/index'
import iframeToggle from './IframeToggle/index'
import useTagsViewStore from '@/store/modules/tagsView'
const route = useRoute()
......@@ -86,4 +88,3 @@ function addIframe() {
border-radius: 3px;
}
</style>
{
"apiAttributeSettingDtoList": [
{
"attributeSettingBizId": "attribute_setting_WlkMorcKwiNbm0Em",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"fieldBizId": "field_UmvLSgtBj3j02JHI",
"fieldValueBizId": "field_value_PBa8PgSYxaYUeIRi",
"name": "是否区分吸烟",
"value": "不区分",
"isCustomize": "0",
"remark": null,
"textBoxType": "select"
},
{
"attributeSettingBizId": "attribute_setting_pP32oWpjgrlF6xan",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"fieldBizId": "field_HEyLqvSBsDXvIWAd",
"fieldValueBizId": "",
"name": "经纪人",
"value": "",
"isCustomize": "0",
"remark": null,
"textBoxType": "text"
},
{
"attributeSettingBizId": "attribute_setting_xEAA2nPCOcAlBcpq",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"fieldBizId": "field_IgjqsyU9oI5FCWFF",
"fieldValueBizId": "field_value_MARgbetAoKpL1jNY",
"name": "性别",
"value": "男",
"isCustomize": "0",
"remark": null,
"textBoxType": "select"
},
{
"attributeSettingBizId": "attribute_setting_EbJWfWYiVgno9MNy",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"fieldBizId": "field_9CmvVH4Zltcjy0Ld",
"fieldValueBizId": "field_value_aPkDpPxZtLEBoqup",
"name": "证件类型",
"value": "身份证",
"isCustomize": "0",
"remark": null,
"textBoxType": "select"
},
{
"attributeSettingBizId": "attribute_setting_TcUdpmiPQUhYL8A3",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"fieldBizId": "field_AEUevz9S6o6jmOtp",
"fieldValueBizId": "",
"name": "保单日期",
"value": "",
"isCustomize": "0",
"remark": null,
"textBoxType": "date"
},
{
"attributeSettingBizId": "attribute_setting_jCOxZccDQLJTESKE",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"fieldBizId": "field_YX8KbhC0NPJfBoRR",
"fieldValueBizId": "",
"name": "颜色",
"value": "field_value_9GeaIYompsO6kizv;field_value_OMwQQKAl7PjLjjuL;field_value_mTCXOVFgWs9WlzRX",
"isCustomize": "0",
"remark": null,
"textBoxType": "multipleSelect"
},
{
"attributeSettingBizId": "attribute_setting_DZ5SjMXlOSXBXwtE",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"fieldBizId": "custom1765265051120",
"fieldValueBizId": "",
"name": "222",
"value": "333",
"isCustomize": "1",
"remark": null,
"textBoxType": null,
"customValue": "自定义参数"
},
{
"attributeSettingBizId": "attribute_setting_pjtvm7lZtFutvgx9",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"fieldBizId": "custom1765265062476",
"fieldValueBizId": "",
"name": "555",
"value": "666",
"isCustomize": "1",
"remark": null,
"textBoxType": null,
"customValue": "自定义参数"
},
{
"attributeSettingBizId": "attribute_setting_04iIGcmvdVBE04rZ",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"fieldBizId": "custom1765266273226",
"fieldValueBizId": "",
"name": "",
"value": "",
"isCustomize": "1",
"remark": null,
"textBoxType": null,
"customValue": "自定义参数"
}
],
"apiSpeciesSettingDtoList": [
{
"isIllustration": "1",
"speciesName": "species_type_IUVxfmyu8RUTHsfq",
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq",
"typeName": "保障计划",
"speciesId": 1765273896521,
"isAdd": false,
"isCustomize": "0",
"value": "保障1",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2"
},
{
"speciesId": 1765273899121,
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq",
"isAdd": false,
"value": "保障2",
"typeName": "保障计划",
"isCustomize": "0",
"isIllustration": "1",
"illustrationUrl": "",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2"
},
{
"isIllustration": "0",
"speciesName": "species_type_TcQNeGLE4rcBcjsj",
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj",
"typeName": "供款年期",
"speciesId": 1765273906553,
"isAdd": false,
"isCustomize": "0",
"value": "供款1",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2"
},
{
"speciesId": 1765273908642,
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj",
"isAdd": false,
"value": "供款2",
"typeName": "供款年期",
"isCustomize": "0",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2"
},
{
"typeName": "颜色",
"value": "红的",
"isIllustration": "0",
"isCustomize": "1",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2"
},
{
"typeName": "颜色",
"value": "黑的",
"isCustomize": "1",
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2"
}
],
"apiSpeciesPriceDtoList": [
{
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"price": 11,
"inventory": 21,
"illustrationUrl": "",
"status": "1",
"apiSpeciesSettingDtoList": [
{
"value": "保障1",
"typeName": "保障计划",
"isIllustration": "1",
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq"
},
{
"value": "供款1",
"typeName": "供款年期",
"isIllustration": "0",
"illustrationUrl": "",
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj"
},
{
"value": "红的",
"typeName": "颜色",
"isIllustration": "0",
"illustrationUrl": ""
}
]
},
{
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"price": 0,
"inventory": 0,
"illustrationUrl": "",
"status": "1",
"apiSpeciesSettingDtoList": [
{
"value": "保障1",
"typeName": "保障计划",
"isIllustration": "1",
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq"
},
{
"value": "供款1",
"typeName": "供款年期",
"isIllustration": "0",
"illustrationUrl": "",
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj"
},
{
"value": "黑的",
"typeName": "颜色",
"isIllustration": "0",
"illustrationUrl": ""
}
]
},
{
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"price": 0,
"inventory": 0,
"illustrationUrl": "",
"status": "1",
"apiSpeciesSettingDtoList": [
{
"value": "保障1",
"typeName": "保障计划",
"isIllustration": "1",
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq"
},
{
"value": "供款2",
"typeName": "供款年期",
"isIllustration": "0",
"illustrationUrl": "",
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj"
},
{
"value": "红的",
"typeName": "颜色",
"isIllustration": "0",
"illustrationUrl": ""
}
]
},
{
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"price": 0,
"inventory": 0,
"illustrationUrl": "",
"status": "1",
"apiSpeciesSettingDtoList": [
{
"value": "保障1",
"typeName": "保障计划",
"isIllustration": "1",
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq"
},
{
"value": "供款2",
"typeName": "供款年期",
"isIllustration": "0",
"illustrationUrl": "",
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj"
},
{
"value": "黑的",
"typeName": "颜色",
"isIllustration": "0",
"illustrationUrl": ""
}
]
},
{
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"price": 0,
"inventory": 0,
"illustrationUrl": "",
"status": "1",
"apiSpeciesSettingDtoList": [
{
"value": "保障2",
"typeName": "保障计划",
"isIllustration": "1",
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq"
},
{
"value": "供款1",
"typeName": "供款年期",
"isIllustration": "0",
"illustrationUrl": "",
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj"
},
{
"value": "红的",
"typeName": "颜色",
"isIllustration": "0",
"illustrationUrl": ""
}
]
},
{
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"price": 0,
"inventory": 0,
"illustrationUrl": "",
"status": "1",
"apiSpeciesSettingDtoList": [
{
"value": "保障2",
"typeName": "保障计划",
"isIllustration": "1",
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq"
},
{
"value": "供款1",
"typeName": "供款年期",
"isIllustration": "0",
"illustrationUrl": "",
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj"
},
{
"value": "黑的",
"typeName": "颜色",
"isIllustration": "0",
"illustrationUrl": ""
}
]
},
{
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"price": 0,
"inventory": 0,
"illustrationUrl": "",
"status": "1",
"apiSpeciesSettingDtoList": [
{
"value": "保障2",
"typeName": "保障计划",
"isIllustration": "1",
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq"
},
{
"value": "供款2",
"typeName": "供款年期",
"isIllustration": "0",
"illustrationUrl": "",
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj"
},
{
"value": "红的",
"typeName": "颜色",
"isIllustration": "0",
"illustrationUrl": ""
}
]
},
{
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"price": 0,
"inventory": 0,
"illustrationUrl": "",
"status": "1",
"apiSpeciesSettingDtoList": [
{
"value": "保障2",
"typeName": "保障计划",
"isIllustration": "1",
"speciesTypeBizId": "species_type_IUVxfmyu8RUTHsfq"
},
{
"value": "供款2",
"typeName": "供款年期",
"isIllustration": "0",
"illustrationUrl": "",
"speciesTypeBizId": "species_type_TcQNeGLE4rcBcjsj"
},
{
"value": "黑的",
"typeName": "颜色",
"isIllustration": "0",
"illustrationUrl": ""
}
]
}
],
"apiProductLaunchDto": {
"productLaunchBizId": "product_launch_cap2EOE86bOCI2Q2",
"productBizId": "product_1001",
"title": "万通保险产品",
"shortTitle": "第一个短标题",
"mainUrlsList": [
"https://yd-ali-oss.oss-cn-shanghai-finance-1-pub.aliyuncs.com/png/2025/11/28/cfafe35897824f28949af08a4c455b10.png",
"https://yd-ali-oss.oss-cn-shanghai-finance-1-pub.aliyuncs.com/png/2025/11/28/6e0de16e242049189605e493c1bcee14.png",
"https://yd-ali-oss.oss-cn-shanghai-finance-1-pub.aliyuncs.com/png/2025/11/28/7215aa9e91b74170a7e5ef918cade0b7.png",
"https://yd-ali-oss.oss-cn-shanghai-finance-1-pub.aliyuncs.com/png/2025/12/09/0b7640e688c64c1f83173da38c7dfec6.png"
],
"detailUrlsList": [
"https://yd-ali-oss.oss-cn-shanghai-finance-1-pub.aliyuncs.com/png/2025/12/09/503f71294ffc43f08dd868e5c9e8fffd.png"
],
"detailDescription": "产品的详细描述",
"status": "DSH",
"isTiming": "1",
"releaseDate": "2025-12-10 02:00:00",
"isHiddenPrice": "1",
"isPurchaseLimit": "1",
"limitDateUnit": "YEARLY",
"limitQuantity": 9,
"projectBizIdList": [
"project_IbjfmMTYvNEBuh2S",
"project_p6OxrioQjvEJD3BZ",
"project_prvciwgfQSRfsbRH",
"project_UEdpnAgfnlmUbVuy"
],
"apiProjectDtoList": [
{
"projectBizId": "project_IbjfmMTYvNEBuh2S",
"projectName": "CSF-小程序",
"projectCode": "PROJECT1019"
},
{
"projectBizId": "project_p6OxrioQjvEJD3BZ",
"projectName": "SW001",
"projectCode": "PROJECT1016"
},
{
"projectBizId": "project_prvciwgfQSRfsbRH",
"projectName": "邮件群发系统",
"projectCode": "PROJECT1018"
},
{
"projectBizId": "project_UEdpnAgfnlmUbVuy",
"projectName": "制作薯片",
"projectCode": "PROJECT1017"
}
],
"categoryBizIdList": [
"category_49nw0brogpbZfwIi",
"category_G0oLWjCx8G788o3Y",
"category_LYKKjcf7O3XG5nz8"
]
}
}
speciesJson
:
"[{\"isIllustration\":\"1\",\"speciesTypeBizId\":\"species_type_IUVxfmyu8RUTHsfq\",\"typeCode\":\"PROTECTION_PLAN\",\"typeName\":\"保障计划\",\"value\":\"70岁\"},{\"illustrationUrl\":\"\",\"isIllustration\":\"0\",\"speciesTypeBizId\":\"species_type_TcQNeGLE4rcBcjsj\",\"typeCode\":\"PAYMENT_TERM\",\"typeName\":\"供款年期\",\"value\":\"3\"},{\"illustrationUrl\":\"\",\"isIllustration\":\"0\",\"speciesTypeBizId\":\"species_type_uvdLgZQHUZQMfv1k\",\"typeCode\":\"SPECIES_TYPE_CODE1616036003\",\"typeName\":\"尺码\",\"value\":\"S\"}]"
status
:
1
......@@ -12,11 +12,10 @@ let downloadLoadingInstance
export let isRelogin = { show: false }
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: import.meta.env.VITE_APP_BASE_API,
// 超时
baseURL: import.meta.env.VITE_APP_BASE_API || '/api', // 使用环境变量
timeout: 10000
})
......@@ -109,7 +108,7 @@ service.interceptors.response.use(
useUserStore()
.logOut()
.then(() => {
location.href = '/index'
location.href = '/login?redirect=/workbench'
})
})
.catch(() => {
......@@ -123,6 +122,9 @@ service.interceptors.response.use(
} else if (code === 601) {
ElMessage({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg))
} else if (code == 1004) {
// 后端自定义的错误码便于前端显示错误信息,非系统性
return Promise.resolve(res.data)
} else if (code !== 200) {
ElNotification.error({ title: msg })
return Promise.reject('error')
......@@ -133,13 +135,37 @@ service.interceptors.response.use(
error => {
console.log('err' + error)
let { message } = error
if (message == 'Network Error') {
message = '后端接口连接异常'
} else if (message.includes('timeout')) {
message = '系统接口请求超时'
} else if (message == 'Request failed with status code 401') {
if (!isRelogin.show) {
isRelogin.show = true
ElMessageBox.confirm('登录状态已过期,请重新登录', '系统提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
showCancelButton: false
})
.then(() => {
isRelogin.show = false
useUserStore()
.logOut()
.then(() => {
location.href = '/login?redirect=/workbench'
})
})
.catch(() => {
isRelogin.show = false
})
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (message.includes('Request failed with status code')) {
message = '系统接口' + message.substr(message.length - 3) + '异常'
}
ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
......
<template>
<div class="app-container">
<div class="form-content">
<div class="formHeader">基础信息</div>
<el-form :model="queryParams" ref="queryRef" label-position="top">
<el-row>
<el-col :span="12">
<div class="commonHeader">产品标题</div>
<el-select
v-model="queryParams.productName"
filterable
remote
allow-create
default-first-option
:reserve-keyword="false"
placeholder="请选择产品名称"
@blur="handleSelectChange"
:remote-method="searchProduct"
clearable
remote-show-suffix
>
<el-option
v-for="item in productList"
:key="item.productBizId"
:label="item.productName"
:value="item.productBizId"
/>
</el-select>
</el-col>
<el-col :span="24">
<div class="nameTip">
<span style="color: #ccc">标题需包含产品名称。</span>
<el-tooltip placement="bottom" effect="light" trigger="click">
<template #content>
<div style="width: 300px">
<div v-for="item in nameRequireList" :key="item.key" style="margin-bottom: 8px">
<span style="font-size: 13px"></span>
<span style="margin-left: 5px">{{ item.label }}</span>
</div>
</div>
</template>
<span style="color: #576b95; cursor: pointer">查看命名要求</span>
</el-tooltip>
</div>
</el-col>
<el-col :span="24" v-if="showNameTip">
<div class="tipCon">
<el-icon color="red" :size="20" style="margin-right: 5px"><Warning /></el-icon>
<span>标题信息过少,请至少输入5个有效字数(含中文、英文、数字)</span>
</div>
</el-col>
<el-col :span="24" style="margin: 20px 0">
<div class="commonHeader">
图片和视频
<span style="color: rgb(111, 111, 111); font-size: 15px"
>({{ imageInfo.count }}/9)</span
>
</div>
<image-upload
v-model="queryParams.picture"
:action="'/oss/api/oss/upload'"
:limit="9"
:image-size="10"
:video-size="500"
:file-type="['png', 'jpg', 'jpeg', 'mp4']"
:is-show-tip="false"
@change="handleImageChange"
@file-change="handleFileChange"
/>
</el-col>
<el-col :span="24">
<div v-for="(item, index) in imageRequireList" :key="item.key" class="imgTip">
<span>{{ index + 1 }}.</span>
<span>{{ item.label }}</span>
<el-tooltip placement="bottom" effect="light" trigger="click" v-if="item.showTip">
<template #content>
<div style="width: 300px">
<div v-for="item in imageTipList" :key="item.key" style="margin-bottom: 8px">
<span style="font-size: 13px"></span>
<span style="margin-left: 5px">{{ item.label }}</span>
</div>
</div>
</template>
<span style="color: #576b95; cursor: pointer">{{ item.tipContent }}</span>
</el-tooltip>
</div>
</el-col>
<el-col :span="24" v-if="imageInfo.count > 0 && imageInfo.count < 3">
<div class="tipCon">
<el-icon color="red" :size="20" style="margin-right: 5px"><Warning /></el-icon>
<span>请上传至少3张产品主图</span>
</div>
</el-col>
<el-col :span="24" style="margin: 20px 0">
<div class="commonHeader">产品类目</div>
<!-- <div class="classTip" v-if="!queryParams.productName || imageInfo.count < 3">
填写标题及上传主图后,即可选择类目
</div> -->
<CategoryTable style="height: 300px" type="add" ref="categoryData" />
</el-col>
</el-row>
</el-form>
</div>
<div class="bottomBtn">
<el-button type="primary" size="large" @click="submitProduct">下一步</el-button>
</div>
</div>
</template>
<script setup name="InsuranceProduct">
import { getProductList, addProduct } from '@/api/product/index'
import useUserStore from '@/store/modules/user'
import { computed, ref } from 'vue'
import ImageUpload from '@/components/ImageUpload/index.vue' //图片上传组件
import CategoryTable from '@/components/CategoryTable/index.vue' //图片上传组件
const userStore = useUserStore()
const router = useRouter()
const { proxy } = getCurrentInstance()
// const { bx_product_type, sys_scope, bx_product_status, bx_smoking_allowed, bx_currency_type } =
// proxy.useDict(
// 'bx_product_type',
// 'sys_scope',
// 'bx_product_status',
// 'bx_smoking_allowed',
// 'bx_currency_type'
// )
const showNameTip = ref(false)
const imageInfo = ref({ count: 0, fileList: [] })
const productList = ref([])
const categoryData = ref(null)
const nameRequireList = ref([
{
label: '标题中必须包含产品名称',
key: '1'
},
{
label: '标题禁止出现连续重复的关键词',
key: '2'
},
{
label: '标题字数不少于5个字,包括中文、 英文、数字',
key: '3'
},
{
label: '标题为非中文时需要附带与产品对应一致的中文翻译',
key: '4'
},
{
label: '标题中需避免填写错别字',
key: '5'
}
])
const imageRequireList = ref([
{
label: '商品主图须符合平台要求,否则将无法通过审核。',
key: '1',
showTip: true,
tipContent: '查看主图要求'
},
{
label: '最少上传3个素材,最多上传9个素材,其中最多上传1个视频(mp4格式),拖拽图片可排序。',
key: '2'
},
{
label: '视频时长不超过180秒,视频大小不超过500M;单张图片需限制在10M以内。',
key: '3'
}
])
const imageTipList = ref([
{
label: '全类目的产品主图要求提供不少于3张产品相关图片',
key: '1'
},
{
label: '主图第一张需为产品实物图',
key: '2'
},
{
label: '产品主图中不可包含相同的图片',
key: '3'
},
{
label: '产品图片不允许为手机截图、空白图',
key: '4'
},
{
label: '图片需清晰,不允许打码、遮挡/ps',
key: '5'
}
])
const data = reactive({
form: {},
queryParams: {},
rules: {
productName: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
scope: [{ required: true, message: '作用域不能为空', trigger: 'blur' }],
productType: [{ required: true, message: '产品类型不能为空', trigger: 'blur' }],
currency: [{ required: true, message: '货币不能为空', trigger: 'blur' }],
premiumRate: [
{ required: true, message: '基础费率不能为空', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === '' || value === null) {
callback(new Error('基础费率不能为空'))
return
}
const numValue = Number(value)
if (isNaN(numValue)) {
callback(new Error('请输入有效的数字'))
} else if (numValue < 0) {
callback(new Error('基础费率不能小于0'))
} else if (numValue > 100) {
callback(new Error('基础费率不能大于100'))
} else {
callback()
}
},
trigger: 'blur'
}
],
smokingAllowed: [{ required: true, message: '是否区分吸烟不能为空', trigger: 'blur' }],
productStatus: [{ required: true, message: '产品状态不能为空', trigger: 'blur' }],
startAge: [
{ required: true, message: '起保年龄不能为空', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!Number.isInteger(Number(value)) || value <= 0) {
callback(new Error('请输入正整数'))
} else {
callback()
}
},
trigger: 'blur'
}
],
endAge: [
{ required: true, message: '终保年龄不能为空', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!Number.isInteger(Number(value)) || value <= 0) {
callback(new Error('请输入正整数'))
} else if (Number(value) <= Number(form.value.startAge)) {
callback(new Error('终保必须大于起保'))
} else {
callback()
}
},
trigger: 'blur'
}
],
tenantBizId: [{ required: true, message: '所属租户不能为空', trigger: 'blur' }],
ssDeptBizIdList: [
{
required: true,
message: '所属保险公司不能为空',
trigger: 'change',
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请至少选择一个所属保险公司'))
} else {
callback()
}
}
}
],
picture: [
{
required: true,
validator: (rule, value, callback) => {
if (!value || value.trim() === '') {
callback(new Error('请上传产品图片'))
} else {
callback()
}
},
trigger: 'change'
}
]
}
})
const { queryParams, form, rules } = toRefs(data)
function handleImageChange(value) {
console.log('图片', value)
}
const handleFileChange = info => {
imageInfo.value = JSON.parse(JSON.stringify(info))
console.log('图片信息', imageInfo.value)
}
const handleSelectChange = () => {
if (queryParams.value.productName && queryParams.value.productName.length < 5) {
showNameTip.value = true
} else if (!queryParams.value.productName) {
showNameTip.value = true
} else {
showNameTip.value = false
}
}
/** 查询产品列表 */
function searchProduct(query) {
let productName = query ? query.trim() : ''
let data = {
productName: productName,
pageSize: 10,
pageNo: 1
}
getProductList(data).then(response => {
if (response.code === 200) {
productList.value = response.data.records
} else {
productList.value = []
proxy.$modal.msgError(response.msg)
}
})
}
/** 新增产品 */
function submitProduct() {
let productBizId = ''
let categoryBizIdList = []
let mainUrlsList = []
let productName = queryParams.value.productName
if (!productName) {
proxy.$modal.msgError('产品名称不能为空或长度小于5')
showNameTip.value = true
return
} else {
productList.value.forEach(item => {
if (item.productBizId === productName) {
productBizId = item.productBizId
productName = item.productName
}
})
}
if (imageInfo.value.fileList && imageInfo.value.fileList.length > 0) {
imageInfo.value.fileList.forEach(item => {
mainUrlsList.push(item.url)
})
} else {
proxy.$modal.msgError('请上传产品图片')
return
}
if (categoryData.value) {
categoryData.value.categoryList.forEach(item => {
item.data.forEach(item2 => {
if (item2.isSelected) {
categoryBizIdList.push(item2.categoryBizId)
}
})
})
}
if (categoryBizIdList.length === 0) {
proxy.$modal.msgError('请选择产品类目')
return
}
let data = {
productBizId: productBizId,
productName: productName,
mainUrlsList: mainUrlsList,
categoryBizIdList: categoryBizIdList
}
// return
addProduct(data).then(response => {
if (response.code === 200) {
router.push({
path: '/merchandise/putOnSale',
query: {
productBizId: response.data.productBizId,
productLaunchBizId: response.data.productLaunchBizId
}
})
} else {
proxy.$modal.msgError(response.msg)
}
})
}
searchProduct()
</script>
<style lang="scss" scoped>
.app-container {
width: 100%;
box-sizing: border-box;
/* background-color: rgb(247 247 247) !important; */
/* padding: 0 !important; */
/* height: 500px !important;
overflow: hidden;
overflow-y: scroll; */
}
.form-content {
width: 100%;
box-sizing: border-box;
height: 72.5vh;
overflow: hidden;
overflow-y: scroll;
/* padding: 20px; */
/* background-color: #fff; */
}
.formHeader {
margin-bottom: 20px;
font-size: 22px;
font-weight: 600;
}
.nameTip {
font-size: 13px;
margin-top: 10px;
}
.tipCon {
/* width: 450px; */
width: fit-content;
/* 为了更好的兼容性,可以加上带前缀的版本 */
width: -moz-fit-content;
width: -webkit-fit-content;
margin: 5px 0;
padding: 10px 10px;
border-radius: 5px;
background-color: rgb(247 247 247);
font-size: 14px;
display: flex;
align-items: center;
span {
color: rgb(111 111 111);
}
}
.commonHeader {
font-size: 16px;
margin-bottom: 15px;
}
.imgTip {
color: #ccc;
font-size: 14px;
margin-bottom: 10px;
}
.classTip {
color: #ccc;
font-size: 14px;
}
.bottomBtn {
border-top: 1px solid rgb(247 247 247);
width: 100%;
padding: 30px 20px;
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #fff;
}
</style>
<template>
<div class="app-container">
<el-row style="margin-bottom: 20px">
<el-col style="display: flex; justify-content: space-between"
><div>
<el-button type="primary" plain icon="Refresh" @click="updateSpecies">更新规格</el-button>
</div>
</el-col>
</el-row>
<el-table
:data="tableData"
:span-method="objectSpanMethod"
border
style="width: 100%; margin-top: 20px"
row-key="rowKey"
>
<!-- 动态生成所有列 -->
<template v-for="column in tableColumns" :key="column.prop">
<el-table-column :prop="column.prop" :label="column.label" />
</template>
<!-- 固定操作列 -->
<el-table-column
label="操作"
:width="tableData.length > 0 ? 180 : 0"
align="center"
fixed="right"
>
<template #default="scope">
<el-button type="primary" size="small" @click="handleCommissionSetting(scope.row)">
佣金设置
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getSpeciesList"
style="margin-top: 10px"
/>
<!-- 佣金设置 -->
<el-dialog
title="佣金设置"
v-model="showCommisionSetting"
width="1200px"
append-to-body
:close-on-click-modal="false"
>
<!-- 表格数据 -->
<el-table v-loading="settingLoading" :data="settingList" border ref="settingTableRef">
<el-table-column
label="序号 "
width="55"
align="center"
type="index"
fixed="left"
></el-table-column>
<el-table-column label="费用名称" prop="expenseName" width="150" fixed="left">
<template #header>
<span class="required-label">费用名称</span>
</template>
<template #default="scope">
<el-select v-model="scope.row.expenseName" placeholder="请选择">
<el-option
v-for="item in commission_cost_type"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="佣金年限(起)" prop="startPeriod" width="120">
<template #header>
<span class="required-label">佣金年限(起)</span>
</template>
<template #default="scope">
<el-input
v-model="scope.row.startPeriod"
type="number"
placeholder="请输入"
:min="1"
clearable
@input="handleNumberInput(scope.row, 'startPeriod', 'positive')"
@blur="handlePeriodBlur(scope.row, scope.$index, 'startPeriod')"
/>
</template>
</el-table-column>
<el-table-column label="佣金年限(止)" prop="endPeriod" width="120">
<template #header>
<span class="required-label">佣金年限(止)</span>
</template>
<template #default="scope">
<el-input
v-model="scope.row.endPeriod"
type="number"
:min="1"
placeholder="请输入"
clearable
@input="handleNumberInput(scope.row, 'endPeriod', 'positive')"
@blur="handlePeriodBlur(scope.row, scope.$index, 'endPeriod')"
/>
</template>
</el-table-column>
<el-table-column label="佣金率(%)" prop="commissionRate" width="120">
<template #header>
<span class="required-label">佣金率(%)</span>
</template>
<template #default="scope">
<el-input
v-model="scope.row.commissionRate"
type="number"
:min="0"
placeholder="请输入"
clearable
@input="handleNumberInput(scope.row, 'commissionRate', 'non-negative')"
/>
</template>
</el-table-column>
<el-table-column label="折标系数(%)" prop="discountRatio" width="120">
<template #header>
<span class="required-label">折标系数(%)</span>
</template>
<template #default="scope">
<el-input
v-model="scope.row.discountRatio"
type="number"
:min="0"
placeholder="请输入"
clearable
@input="handleNumberInput(scope.row, 'discountRatio', 'non-negative')"
/>
</template>
</el-table-column>
<el-table-column label="生效日期(起)" prop="effectiveStart" width="150">
<template #header>
<span class="required-label">生效日期(起)</span>
</template>
<template #default="scope">
<el-date-picker
v-model="scope.row.effectiveStart"
style="width: 100%"
type="date"
placeholder="请选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleStartDateChange(scope.row, scope.$index)"
/>
</template>
</el-table-column>
<el-table-column label="生效日期(止)" prop="effectiveEnd" width="150">
<template #header>
<span class="required-label">生效日期(止)</span>
</template>
<template #default="scope">
<el-date-picker
v-model="scope.row.effectiveEnd"
style="width: 100%"
type="date"
placeholder="请选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleEndDateChange(scope.row, scope.$index)"
/>
</template>
</el-table-column>
<el-table-column label="是否受汇率影响" prop="isExchangeRate" width="150">
<template #header>
<span class="required-label">是否受汇率影响</span>
</template>
<template #default="scope">
<el-select
v-model="scope.row.isExchangeRate"
style="width: 100%"
placeholder="请选择"
clearable
>
<el-option
v-for="item in sys_no_yes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="结算币种" prop="currency" width="120">
<template #header>
<span class="required-label">结算币种</span>
</template>
<template #default="scope">
<el-select
v-model="scope.row.currency"
style="width: 100%"
placeholder="请选择"
clearable
>
<el-option
v-for="item in bx_currency_type"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="对账公司"
prop="reconciliationCompanyName"
align="center"
width="150"
>
<template #header>
<span class="required-label">对账公司</span>
</template>
<template #default="scope">
<el-select
v-model="scope.row.reconciliationCompanyName"
filterable
remote
reserve-keyword
placeholder="请输入关键词搜索"
:remote-method="query => searchSelectList(query, 'reconciliationCompanyName')"
:loading="searchLoadingStates['reconciliationCompanyName']"
>
<el-option
v-for="item in searchOptions['reconciliationCompanyName'] || []"
:key="item.id"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="销售组织" prop="salesOrgName" align="center" width="150">
<template #header>
<span class="required-label">销售组织</span>
</template>
<template #default="scope">
<el-select
v-model="scope.row.salesOrgName"
filterable
remote
reserve-keyword
placeholder="请输入关键词搜索"
:remote-method="query => searchSelectList(query, 'salesOrgName')"
:loading="searchLoadingStates['salesOrgName']"
>
<el-option
v-for="item in searchOptions['salesOrgName'] || []"
:key="item.id"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="状态" prop="status" width="120">
<template #header>
<span class="required-label">状态</span>
</template>
<!-- <template #default="scope">
<el-select
v-model="scope.row.status"
style="width: 100%"
placeholder="请选择"
clearable
>
<el-option
v-for="item in sys_status"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template> -->
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="1"
inactive-value="0"
@change="switchChange(scope.row)"
/>
<span style="margin-left: 8px">
{{ scope.row.status === '1' ? '启用' : '禁用' }}
</span>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" width="200">
<template #default="scope">
<el-input v-model="scope.row.remark" type="textarea" placeholder="请输入" />
</template>
</el-table-column>
<el-table-column label="最近一次操作人" prop="updaterName" width="120" />
<el-table-column label="最近一次操作时间" prop="updateTime" width="160">
<template #default="scope">
<span>{{ scope.row.updateTime ? parseTime(scope.row.updateTime) : '--' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<div class="btnCon">
<el-button
v-if="scope.$index == settingList.length - 1"
@click="handleAddCommission(scope.row)"
text
size="small"
type="primary"
>新增</el-button
>
<el-button
text
size="small"
type="danger"
@click="handleDeleteCommission(scope.row, scope.$index)"
>删除</el-button
>
</div>
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeSetting">关闭</el-button>
<el-button type="primary" @click="submitSetting">提 交</el-button>
</div>
</template>
</el-dialog>
<el-dialog title="佣金设置错误提示" v-model="errorTip" width="500px" append-to-body>
<div style="margin-bottom: 10px" v-for="item in settingErrorTip">{{ item }}</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="errorTip = false">去修改</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="SendInfo">
import { cloneDeep } from 'lodash-es'
import {
comeCommissionRatio,
comeCommissionList,
comeExpectedSpecies,
comeBatchSave,
deleteComeCommission,
changeComeStatus
} from '@/api/product/index'
import { getAllCompanys } from '@/api/common'
import { ref, watch } from 'vue'
const props = defineProps({
// 类型,是新增还是编辑,
activeName: {
type: String,
default: ''
}
})
const route = useRoute()
const { proxy } = getCurrentInstance()
const loading = ref(true)
const settingLoading = ref(true)
const total = ref(0) //规格表格的总条数
const settingTotal = ref(0) //规格表格的总条数
const showCommisionSetting = ref(false) //佣金设置弹窗开关
const errorTip = ref(false) //佣金设置弹窗开关
const settingList = ref([]) //佣金设置表格数据
const currentSettingRow = ref([]) //当前选中的佣金设置行
const settingErrorTip = ref([]) //提交佣金错误提示信息
const searchOptions = ref({}) // 存储不同key对应的选项
const searchLoadingStates = ref({}) // 存储不同key对应的加载状态
const data = reactive({
queryParams: {
pageNo: 1,
pageSize: 10,
productLaunchBizId: route.query.productLaunchBizId
},
settingQueryParams: {
pageNo: 1,
pageSize: 9999
}
})
const { queryParams, settingQueryParams } = toRefs(data)
const { bx_currency_type, commission_cost_type, sys_no_yes, sys_status } = proxy.useDict(
'bx_currency_type',
'commission_cost_type',
'sys_no_yes',
'sys_status'
)
const switchChange = row => {
if (!row.expectedCommissionRatioBizId) {
return
}
try {
changeComeStatus({
expectedCommissionRatioBizId: row.expectedCommissionRatioBizId,
status: Number(row.status)
}).then(response => {
if (response.code === 200) {
proxy.$modal.msgSuccess('状态修改成功')
}
})
} catch (error) {
row.status = row.status === '1' ? '0' : '1'
}
}
// 搜索方法
const searchSelectList = async (query, fieldKey) => {
// 设置该字段的加载状态
searchLoadingStates.value[fieldKey] = true
try {
// 根据不同的字段key调用不同的API
if (fieldKey === 'reconciliationCompanyName' || fieldKey === 'salesOrgName') {
const params = {
deptName: query.trim(),
pageNo: 1,
pageSize: 10
}
getAllCompanys(params).then(response => {
response.data.records = response.data.records.map(item => {
return {
...item,
label: item.deptName,
value: item.deptBizId
}
})
searchOptions.value[fieldKey] = response.data.records
})
}
} catch (error) {
console.error(`${fieldKey} 搜索失败`, error)
searchOptions.value[fieldKey] = []
} finally {
searchLoadingStates.value[fieldKey] = false
}
}
const handleDeleteCommission = (row, index) => {
if (settingList.value.length === 1) {
proxy.$modal.msgError('至少保留一条佣金设置数据')
return
}
proxy.$modal
.confirm(`是否确认删除第${index + 1}行数据?`)
.then(function () {
if (row.expectedCommissionRatioBizId) {
deleteComeCommission(row.expectedCommissionRatioBizId).then(response => {
if (response.code === 200) {
settingList.value.splice(index, 1)
} else {
proxy.$modal.msgError(response.msg)
}
})
} else {
settingList.value.splice(index, 1)
}
proxy.$modal.msgSuccess('删除成功')
})
.catch(() => {})
}
const handleNumberInput = (row, field, type = 'positive') => {
const value = row[field]
if (value === null || value === undefined || value === '') return
let numValue = Number(value)
if (isNaN(numValue)) {
row[field] = null
return
}
if (type === 'positive') {
// 正整数:移除负号和小数部分
if (numValue <= 0) {
row[field] = null
return
}
// 取整数部分
row[field] = Math.floor(Math.abs(numValue))
} else if (type === 'non-negative') {
// 非负数:允许0及以上,不允许负数
if (numValue < 0) {
row[field] = 0
}
}
}
// 处理开始日期变化
const handleStartDateChange = (row, index) => {
if (row.effectiveStart && row.effectiveEnd) {
const start = new Date(row.effectiveStart)
const end = new Date(row.effectiveEnd)
if (start > end) {
proxy.$modal.msgError(
`第${index + 1}行:生效日期(起)不能晚于生效日期(止),请重新选择生效日期(起)`
)
// 清空结束日期,让用户重新选择
setTimeout(() => {
row.effectiveStart = ''
}, 0)
}
}
}
// 处理结束日期变化
const handleEndDateChange = (row, index) => {
console.log('时间', row)
if (row.effectiveStart && row.effectiveEnd) {
const start = new Date(row.effectiveStart)
const end = new Date(row.effectiveEnd)
if (end < start) {
proxy.$modal.msgError(
`第${index + 1}行:生效日期(止)不能早于生效日期(起),请重新选择生效日期(止)`
)
// 清空开始日期,让用户重新选择
setTimeout(() => {
row.effectiveEnd = ''
}, 0)
}
}
}
const handlePeriodBlur = (row, index, key) => {
if (row.startPeriod && row.endPeriod && row.startPeriod > row.endPeriod) {
proxy.$modal.msgError(`第${index + 1}行:佣金开始年限不能大于佣金结束年限,请重新填写`)
row[key] = ''
row.unadd = true
} else {
row.unadd = false
}
console.log('年限', row)
}
const handleAddCommission = row => {
let newRow = cloneDeep(row)
let obj = {}
// 复制当前条的数据
for (const key in newRow) {
if (key !== 'startPeriod' && key !== 'endPeriod' && key !== 'commissionRate') {
obj[key] = newRow[key]
} else {
obj[key] = ''
}
}
if (!row.unadd) {
settingList.value.push(obj)
}
}
const closeSetting = () => {
showCommisionSetting.value = false
settingQueryParams.value = {
pageNo: 1,
pageSize: 9999
}
}
// 批量提交佣金设置
const submitSetting = async () => {
// 校验表格数据
const validationResult = validateTableData()
if (!validationResult.valid) {
proxy.$modal.msgError(validationResult.message)
return
}
let saveTable = cloneDeep(settingList.value)
saveTable.forEach(item => {
if (item.reconciliationCompanyName) {
searchOptions.value['reconciliationCompanyName'].forEach(item1 => {
if (item.reconciliationCompanyName == item1.value) {
item.reconciliationCompanyName = item1.label
item.reconciliationCompany = item1.value
}
})
}
if (item.salesOrgName) {
searchOptions.value['salesOrgName'].forEach(item1 => {
if (item.salesOrgName == item1.value) {
item.salesOrgName = item1.label
item.salesOrg = item1.value
}
})
}
})
// return
try {
comeBatchSave({
expectedSpeciesBizId: currentSettingRow.value.expectedSpeciesBizId,
ratioBatchSaveDtoList: saveTable
}).then(response => {
console.log('response', response)
if (response.code === 200) {
showCommisionSetting.value = false
proxy.$modal.msgSuccess('提交成功')
} else if (response.code === 1004) {
if (response.msg) {
settingErrorTip.value = response.msg.split('\n').filter(item => item !== '')
}
errorTip.value = true
}
})
} catch (error) {
console.log('错误了', error)
}
}
// 表格数据校验函数
const validateTableData = () => {
const errors = []
const requiredFields = [
{ key: 'expenseName', label: '费用名称' },
{ key: 'startPeriod', label: '佣金年限(起)' },
{ key: 'endPeriod', label: '佣金年限(止)' },
{ key: 'commissionRate', label: '佣金率(%)' },
{ key: 'discountRatio', label: '折标系数(%)' },
{ key: 'effectiveStart', label: '生效日期(起)' },
{ key: 'effectiveEnd', label: '生效日期(止)' },
{ key: 'isExchangeRate', label: '是否受汇率影响' },
{ key: 'currency', label: '结算币种' },
{ key: 'salesOrgName', label: '销售组织' },
{ key: 'reconciliationCompanyName', label: '对账公司' }
]
settingList.value.forEach((row, index) => {
requiredFields.forEach(field => {
const value = row[field.key]
let isValid = true
if (Array.isArray(value)) {
isValid = value.length > 0
} else {
isValid = value !== null && value !== undefined && value !== ''
}
if (!isValid) {
errors.push({
rowIndex: index,
field: field.key,
label: field.label,
rowNumber: index + 1
})
}
})
})
if (errors.length === 0) {
return { valid: true }
}
// 生成友好的错误提示信息
const errorMessages = errors
.slice(0, 3)
.map(error => `第${error.rowNumber}行【${error.label}】未填写`)
let message = errorMessages.join(';')
if (errors.length > 3) {
message += `等,共${errors.length}处必填项未填写`
}
return {
valid: false,
errors,
message
}
}
/** 获得佣金设置数据 */
function getSettingList() {
settingLoading.value = true
let obj = {
expenseName: '',
startPeriod: '',
endPeriod: '',
commissionRate: '',
discountRatio: '',
effectiveStart: '',
effectiveEnd: '',
isExchangeRate: '',
currency: '',
status: '1',
remark: '',
salesOrgName: '',
reconciliationCompanyName: ''
}
settingList.value = []
settingQueryParams.value.expectedSpeciesBizId = currentSettingRow.value.expectedSpeciesBizId
comeCommissionRatio(settingQueryParams.value).then(response => {
if (response.code === 200) {
if (response.data.records.length > 0) {
settingList.value = response.data.records
settingList.value.forEach(row => {
row.status = row.status === 1 ? '1' : row.status === 0 ? '0' : ''
})
} else {
settingList.value.push(obj)
}
settingTotal.value = response.data.total
settingLoading.value = false
showCommisionSetting.value = true
} else {
settingLoading.value = false
proxy.$modal.msgError(response.msg)
}
})
}
// 原始数据
const originalData = ref([])
// 表格列配置
const tableColumns = ref([])
// 表格数据
const tableData = ref([])
// 合并行配置,存储每列需要合并的行数
const spanConfig = ref({})
// 从数据中提取所有可能的列
const extractColumnsFromData = data => {
const columnSet = new Map()
data.forEach(item => {
try {
if (item.apiSpeciesSettingDtoList && item.apiSpeciesSettingDtoList.length > 0) {
item.apiSpeciesSettingDtoList.forEach(species => {
if (!columnSet.has(species.typeCode)) {
columnSet.set(species.typeCode, {
prop: species.typeCode,
label: species.typeName,
width: species.typeName.length * 15 + 30,
align: 'center',
sortable: false
})
}
})
}
} catch (e) {
console.log('计算列失败', e)
}
})
// 将 Map 转换为数组并按 typeCode 排序(可选)
return Array.from(columnSet.values())
}
// 处理数据,生成表格需要的格式
const processTableData = () => {
if (!originalData.value || originalData.value.length === 0) {
tableData.value = []
spanConfig.value = {}
return
}
// 1. 提取所有列
const columns = extractColumnsFromData(originalData.value)
tableColumns.value = columns
// 2. 转换数据格式
const processedData = []
originalData.value.forEach((item, index) => {
const rowData = {
rowKey: `${item.id}_${index}`, // 生成唯一 rowKey
id: item.id, // 保留原始ID
rawData: item // 保留原始数据
}
// 填充所有列的值
columns.forEach(column => {
try {
if (item.apiSpeciesSettingDtoList && item.apiSpeciesSettingDtoList.length > 0) {
const matchedSpecies = item.apiSpeciesSettingDtoList.find(s => s.typeCode === column.prop)
rowData[column.prop] = matchedSpecies ? matchedSpecies.value : ''
} else {
rowData[column.prop] = ''
}
} catch (e) {
console.log('填充所有列的值失败', e)
}
})
processedData.push(rowData)
})
// 3. 计算合并配置
calculateSpanConfig(processedData, columns)
tableData.value = processedData
}
// 计算合并配置
const calculateSpanConfig = (data, columns) => {
const config = {}
// 为每一列计算合并配置
columns.forEach((column, columnIndex) => {
config[column.prop] = []
let pos = 0
while (pos < data.length) {
const currentValue = data[pos][column.prop]
let rowspan = 1
// 向后查找相同值的行
for (let i = pos + 1; i < data.length; i++) {
// 只有当该列之前的所有列的值都相同时才合并
let shouldMerge = true
for (let j = 0; j < columnIndex; j++) {
const prevColumn = columns[j]
if (data[pos][prevColumn.prop] !== data[i][prevColumn.prop]) {
shouldMerge = false
break
}
}
if (shouldMerge && data[i][column.prop] === currentValue) {
rowspan++
} else {
break
}
}
// 记录合并配置
config[column.prop][pos] = rowspan
// 跳过已经计算的行
pos += rowspan
}
// 填充剩余的配置为0(不合并)
for (let i = 0; i < data.length; i++) {
if (!config[column.prop][i]) {
config[column.prop][i] = 0
}
}
})
spanConfig.value = config
}
// 合并单元格的方法
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
// 最后一列是操作列,不合并
if (columnIndex === tableColumns.value.length) {
return {
rowspan: 1,
colspan: 1
}
}
const columnProp = tableColumns.value[columnIndex].prop
const rowspan = spanConfig.value[columnProp]?.[rowIndex] || 0
if (rowspan > 0) {
return {
rowspan: rowspan,
colspan: 1
}
} else {
return {
rowspan: 0,
colspan: 0
}
}
}
// 佣金设置操作
const handleCommissionSetting = row => {
currentSettingRow.value = row.rawData
console.log(' currentSettingRow.value', currentSettingRow.value)
getSettingList()
}
/** 获得规格数据 */
function getSpeciesList() {
loading.value = true
comeCommissionList(queryParams.value).then(response => {
if (response.code === 200) {
originalData.value = response.data.records
total.value = response.data.total
loading.value = false
if (originalData.value.length == 0) {
proxy.$modal.msgError('该商品暂无规格数据,请先更新规格数据')
} else {
let codeArr = []
// 自定义没有typeCode影响表格得展示
originalData.value[0].apiSpeciesSettingDtoList.forEach((item, index) => {
if (item.isCustomize == '1') {
item.typeCode = `custom${Date.now() + Math.floor(Math.random() * 1000)}`
codeArr.push({ index: index, code: item.typeCode })
}
})
originalData.value.forEach((item, oIndex) => {
item.apiSpeciesSettingDtoList.forEach((species, sIndex) => {
if (codeArr.length > 0) {
codeArr.forEach(codeItem => {
if (codeItem.index == sIndex) {
species.typeCode = codeItem.code
}
})
}
})
})
}
// 计算表格
processTableData()
searchSelectList('', 'reconciliationCompanyName')
searchSelectList('', 'salesOrgName')
} else {
proxy.$modal.msgError(response.msg)
}
})
}
/** 更新规格数据 */
function updateSpecies() {
loading.value = true
comeExpectedSpecies({ productLaunchBizId: route.query.productLaunchBizId }).then(response => {
if (response.code === 200) {
proxy.$modal.msgSuccess('来佣管理-规格更新成功')
getSpeciesList()
} else {
proxy.$modal.msgError(response.msg)
}
})
}
//========多选下拉框悬停效果结束=========
watch(
() => props.activeName,
newVal => {
if (newVal == 'comeCommission') {
getSpeciesList()
}
}
)
</script>
<style lang="scss" scoped>
.required-label::before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
.btnCon {
display: flex;
align-items: center;
justify-content: center;
}
/* 错误行样式 */
.error-row {
background-color: #fff2f0 !important;
animation: highlight 0.6s ease-in-out;
}
/* 错误字段高亮 */
.error-field :deep(.el-input__wrapper) {
border-color: #f56c6c !important;
box-shadow: 0 0 0 1px #f56c6c !important;
}
.error-field :deep(.el-input__inner) {
color: #f56c6c;
}
.error-field :deep(.el-select__wrapper) {
border-color: #f56c6c !important;
box-shadow: 0 0 0 1px #f56c6c !important;
}
/* 闪烁动画吸引用户注意力 */
@keyframes highlight {
0%,
50% {
background-color: #fff2f0;
}
25%,
75% {
background-color: #ffeaea;
}
100% {
background-color: #fff2f0;
}
}
/* 错误提示消息样式 */
.validation-message-box {
:deep(.el-message-box__content) {
max-height: 400px;
overflow-y: auto;
}
}
</style>
<template>
<div class="baseBox">
<div class="form-content">
<!-- <div class="formHeader">基础信息</div> -->
<el-form>
<!-- 基础信息 -->
<el-card class="cardStyle" v-if="showImage">
<template #header>
<div @click="categoryDialogOpen = true">
<span class="cardTitle">基础信息</span>
<span class="chooseClass" v-if="categoryName">{{ categoryName }}</span>
<span class="editTxt">修改</span>
</div>
</template>
<div>
<el-row>
<el-col :span="12">
<div class="commonHeader">产品标题</div>
<el-select
v-model="form['apiProductLaunchDto'].title"
filterable
remote
allow-create
default-first-option
:reserve-keyword="false"
placeholder="请选择产品名称"
@blur="handleSelectChange"
:remote-method="searchProduct"
clearable
remote-show-suffix
>
<el-option
v-for="item in productList"
:key="item.productBizId"
:label="item.productName"
:value="item.productBizId"
/>
</el-select>
</el-col>
<el-col :span="24" :class="showNameTip ? '' : 'colBottomGap'">
<div class="nameTip">
<span style="color: #ccc">标题需包含产品名称。</span>
<el-tooltip placement="bottom" effect="light" trigger="click">
<template #content>
<div style="width: 300px">
<div
v-for="item in nameRequireList"
:key="item.key"
style="margin-bottom: 8px"
>
<span style="font-size: 13px"></span>
<span style="margin-left: 5px">{{ item.label }}</span>
</div>
</div>
</template>
<span style="color: #576b95; cursor: pointer">查看命名要求</span>
</el-tooltip>
</div>
</el-col>
<el-col :span="24" v-if="showNameTip">
<div class="tipCon">
<el-icon color="red" :size="20" style="margin-right: 5px"><Warning /></el-icon>
<span>标题信息过少,请至少输入5个有效字数(含中文、英文、数字)</span>
</div>
</el-col>
<el-col :span="12">
<div class="commonHeader">产品短标题</div>
<el-input
v-model="form['apiProductLaunchDto'].shortTitle"
placeholder="请输入"
maxlength="20"
show-word-limit
/>
</el-col>
<el-col :span="24" class="colBottomGap">
<div class="nameTip">
<span style="color: #ccc">短标题会展示在对话、朋友圈分享等场景。</span>
</div>
</el-col>
<el-col :span="24">
<div class="commonHeader">产品参数</div>
<el-row :gutter="20">
<template v-if="form.apiAttributeSettingDtoList.length">
<el-col
:span="item.isCustomize == '1' ? 24 : 8"
v-for="(item, index) in form.apiAttributeSettingDtoList"
>
<div
class="paramsItem"
v-if="item.isCustomize == '0'"
:style="{ marginBottom: item.errorMsg ? '0px' : '20px' }"
>
<div class="paramsLeft" style="width: 30%">
<el-select
v-model="item.name"
placeholder="请选择"
@change="changeLeft(item, index)"
>
<el-option
v-for="left in item.leftOptions"
:key="left.value"
:label="left.fieldCnName"
:value="left.fieldBizId"
/>
</el-select>
</div>
<div class="paramsMiddle" style="width: 63%">
<el-input
v-model="item.value"
placeholder="请输入"
@blur="paramsErr"
v-if="item.textBoxType == 'text'"
/>
<el-input-number
v-if="item.textBoxType == 'number'"
v-model="item.value"
:min="1"
style="width: 100%"
/>
<!-- 单选下拉框 -->
<el-select
v-model="item.value"
@blur="paramsErr"
placeholder="请选择"
v-if="item.textBoxType == 'select'"
>
<el-option
v-for="middle in item.middleOptions"
:key="middle.fieldValueBizId"
:label="middle.value"
:value="middle.fieldValueBizId"
clearable
/>
</el-select>
<!-- 多选下拉框 -->
<el-select
v-model="item.value"
@blur="paramsErr"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
placeholder="请选择"
v-if="item.textBoxType == 'multipleSelect'"
clearable
>
<el-option
v-for="middle in item.middleOptions"
:key="middle.fieldValueBizId"
:label="middle.value"
:value="middle.fieldValueBizId"
/>
</el-select>
<!-- 带时分的时间框 -->
<el-date-picker
@blur="paramsErr"
v-model="item.value"
style="width: 100%"
v-if="item.textBoxType == 'dateTime'"
type="datetime"
placeholder="请选择"
@change="handleDateChange()"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<!-- 不带时分 -->
<el-date-picker
@blur="paramsErr"
v-model="item.value"
style="width: 100%"
v-if="item.textBoxType == 'date'"
type="date"
placeholder="请选择"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleDateChange()"
/>
</div>
<div
class="paramsRight"
style="width: 8%"
@click="deleteParams(item, index)"
>
<el-icon><Delete /></el-icon>
</div>
</div>
<div
class="paramsItem"
v-else
:style="{ marginBottom: item.errorMsg ? '0px' : '20px' }"
>
<div class="paramsLeft" style="width: 30%">
<el-select
v-model="item.customValue"
placeholder="请选择"
@change="changeCustomLeft(item, index)"
>
<el-option
v-for="left in item.leftOptions"
:key="left.value"
:label="left.fieldCnName"
:value="left.fieldBizId"
/>
</el-select>
</div>
<div class="paramsMiddle" style="width: 63%">
<el-input
v-model="item.name"
placeholder="请输入参数名"
@blur="paramsErr"
/>
</div>
<div class="paramsMiddle customMiddle" style="width: 63%">
<el-input
v-model="item.value"
placeholder="请输入参数值"
@blur="paramsErr"
/>
</div>
<div
class="paramsRight"
style="width: 8%"
@click="deleteCustomParams(item, index)"
>
<el-icon><Delete /></el-icon>
</div>
</div>
<div class="errorParams" v-if="item.errorMsg">
<el-icon color="red" :size="20" style="margin-right: 5px"
><Warning
/></el-icon>
<span>{{ item.errorMsg }}</span>
</div>
</el-col>
</template>
<el-col :span="24" class="addTxt">
<el-icon :size="24"><CirclePlus /></el-icon>
<span style="margin-left: 5px" @click="addParams">添加新参数</span>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</el-card>
<!-- 配图 -->
<el-card class="cardStyle" v-if="showImage">
<template #header>
<div>
<span class="cardTitle">产品展示与介绍</span>
</div>
</template>
<div>
<el-row>
<!-- 主图 -->
<el-col :span="24" style="margin: 20px 0">
<div class="commonHeader">
图片和视频
<span style="color: rgb(111, 111, 111); font-size: 15px"
>({{ imageInfo.count }}/9)</span
>
</div>
<image-upload
v-model="form['apiProductLaunchDto'].mainUrlsList"
:action="'/oss/api/oss/upload'"
:limit="9"
:image-size="10"
:video-size="500"
:file-type="['png', 'jpg', 'jpeg', 'mp4']"
:is-show-tip="false"
source="main"
@file-change="handleFileChange"
/>
</el-col>
<el-col :span="24">
<div v-for="(item, index) in imageRequireList" :key="item.key" class="imgTip">
<span>{{ index + 1 }}.</span>
<span>{{ item.label }}</span>
<el-tooltip placement="bottom" effect="light" trigger="click" v-if="item.showTip">
<template #content>
<div style="width: 300px">
<div
v-for="item in imageTipList"
:key="item.key"
style="margin-bottom: 8px"
>
<span style="font-size: 13px"></span>
<span style="margin-left: 5px">{{ item.label }}</span>
</div>
</div>
</template>
<span style="color: #576b95; cursor: pointer">{{ item.tipContent }}</span>
</el-tooltip>
</div>
</el-col>
<el-col :span="24" v-if="imageInfo.count > 0 && imageInfo.count < 3">
<div class="tipCon">
<el-icon color="red" :size="20" style="margin-right: 5px"><Warning /></el-icon>
<span>请上传至少3张产品主图</span>
</div>
</el-col>
<!-- 详情图 -->
<el-col :span="24" style="margin: 20px 0">
<div class="commonHeader">
详情图和描述
<span style="color: rgb(111, 111, 111); font-size: 15px"
>({{ subImageInfo.count }}/50)</span
>
</div>
<image-upload
v-model="form['apiProductLaunchDto'].detailUrlsList"
:action="'/oss/api/oss/upload'"
:limit="50"
:image-size="10"
:file-type="['png', 'jpg', 'jpeg']"
:is-show-tip="false"
source="sub"
@file-change="handleFileChange"
/>
</el-col>
<el-col :span="24">
<div v-for="(item, index) in desRequireList" :key="item.key" class="imgTip">
<span>{{ index + 1 }}.</span>
<span>{{ item.label }}</span>
<el-tooltip placement="bottom" effect="light" trigger="click" v-if="item.showTip">
<template #content>
<div style="width: 310px">
<div v-for="item in desTipList" :key="item.key" style="margin-bottom: 8px">
<span style="font-size: 13px"></span>
<span style="margin-left: 5px">{{ item.label }}</span>
</div>
</div>
</template>
<span style="color: #576b95; cursor: pointer">{{ item.tipContent }}</span>
</el-tooltip>
</div>
</el-col>
<el-col :span="24" v-if="subImageInfo.count == 0">
<div class="tipCon">
<el-icon color="red" :size="20" style="margin-right: 5px"><Warning /></el-icon>
<span>请上传至少1张产品详情图</span>
</div>
</el-col>
<el-col :span="24" style="margin-top: 10px">
<el-input
v-model="form['apiProductLaunchDto'].detailDescription"
type="textarea"
placeholder="产品描述(选填)"
/>
</el-col>
</el-row>
</div>
</el-card>
<!-- 规格和库存价格 -->
<el-card class="cardStyle" v-if="showImage">
<template #header>
<div>
<span class="cardTitle">规格和库存价格</span>
</div>
</template>
<div>
<el-row>
<el-col :span="24">规格</el-col>
<el-col :span="24">
<div
class="speciesItem"
v-for="(item, index) in form.speciesData"
:key="item.speciesTypeBizId"
>
<div class="speciesTop">
<div class="speciesTopLeft">
<el-select
v-model="item.speciesName"
placeholder="请选择"
@change="changeSpeciesSelect(item, index)"
style="width: 300px"
>
<el-option
v-for="left in item.options"
:key="left.speciesTypeBizId"
:label="left.typeName"
:value="left.speciesTypeBizId"
/>
</el-select>
<!-- 自定义规格名称 -->
<div class="customizeInput" v-if="item.isCustomize == '1'">
<el-input
v-model="item.typeName"
placeholder="请输入规格名称"
@blur="changCustomSpecies(item)"
style="width: 300px"
/>
<div class="customizeErrorMsg" v-if="item.errorMsg">
{{ item.errorMsg }}
</div>
</div>
<!-- 切换配图得switch -->
<template v-if="index == 0">
<el-popconfirm
v-if="item.isIllustration == '1'"
width="300"
class="box-item"
title="关闭开关会清空现在的配图"
placement="bottom"
hide-icon
@confirm="switchChange(item, 'close')"
>
<template #reference>
<div>
<el-switch
v-model="item.isIllustration"
active-value="1"
inactive-value="0"
style="margin: 0 5px 0px 15px"
:before-change="switchBeforeChange"
></el-switch>
<span class="imgSwitch">配图</span>
</div>
</template>
</el-popconfirm>
<div v-else>
<el-switch
v-model="item.isIllustration"
active-value="1"
inactive-value="0"
style="margin: 0 5px 0px 15px"
@change="switchChange(item, 'open')"
></el-switch>
<span class="imgSwitch">配图</span>
</div>
</template>
</div>
<div class="speciesTopRight" @click="deleteSpecies(item)">
<el-icon :size="18"><Delete /></el-icon>
</div>
</div>
<el-row :gutter="20" style="margin-top: 15px">
<el-col
:span="8"
v-for="(species, sIndex) in item.apiSpeciesSettingDtoList"
:key="species.speciesId"
>
<div class="paramsItem" style="margin-bottom: 20px">
<div
class="paramsLeft speciesLeft"
style="width: 8%"
v-if="species.isIllustration == '1'"
>
<el-upload
:action="uploadUrl"
style="display: flex"
list-type="picture"
:headers="headers"
:show-file-list="false"
:on-success="(res, file) => handleUploadSuccess(res, file, species)"
:before-upload="handleBeforeUpload"
>
<el-icon v-if="!species.illustrationUrl" :size="24" color="#969696"
><Picture
/></el-icon>
<!-- <el-popconfirm
width="300"
class="box-item"
title="关闭开关会清空现在的配图"
placement="bottom"
hide-icon
>
</el-popconfirm> -->
<img v-else :src="species.illustrationUrl" style="width: 100%; height:
32px;object-fit: cover"
</el-upload>
</div>
<div
class="paramsMiddle speciesMiddle"
:style="{ width: getSpeciesMiddleWidth(species, item) }"
>
<el-input
v-model="species.value"
placeholder="请输入"
@keydown="speciesInputChange(species, sIndex, item)"
/>
</div>
<div
v-if="item.apiSpeciesSettingDtoList.length > 1"
class="paramsRight speciesRight"
style="width: 8%"
@click="deleteSpeciesValue(species)"
>
<el-icon><Delete /></el-icon>
</div>
</div>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="24" class="addTxt">
<el-icon :size="22"><CirclePlus /></el-icon>
<span style="margin-left: 5px" @click="addSpecies">创建新规格</span>
</el-col>
<el-col :span="24" style="margin-top: 20px">价格与库存</el-col>
<!-- 规格价格得设置 -->
<el-col
:span="24"
style="margin-top: 10px; margin-bottom: 10px"
v-if="speciesSelectList.length > 0"
>
<!-- 规格值得选择 -->
<div class="speciesSelect">
<div v-for="item in speciesSelectList">
<el-select
placeholder="请选择"
style="width: 220px; margin-right: 20px"
v-model="selectedValues[item.speciesTypeBizId]"
>
<el-option
v-for="left in item.options"
:key="left.speciesId"
:label="left.value"
:value="left.speciesId"
/>
</el-select>
</div>
</div>
<!-- 产品价格等得固定设置 -->
<div class="settingBox">
<div class="paramsItem settingItem">
<div class="paramsLeft settingLeft">售卖价</div>
<div class="paramsMiddle settingRight">
<el-input type="number" v-model="batchPrice" placeholder="请输入售卖价" />
</div>
</div>
<div class="paramsItem settingItem">
<div class="paramsLeft settingLeft">库存</div>
<div class="paramsMiddle settingRight">
<el-input type="number" v-model="batchInventory" placeholder="请输入库存" />
</div>
</div>
<el-button
type="primary"
plain
@click="handleBatchSet"
:disabled="!batchPrice && !batchInventory"
>设置</el-button
>
</div>
</el-col>
<!-- 规格表格部分 -->
<el-table
:data="tableData"
:span-method="objectSpanMethod"
border
style="width: 100%; margin-top: 20px"
height="250"
>
<!-- 动态规格列 -->
<el-table-column
v-for="spec in form.speciesData.filter(
item => item.typeName && item.apiSpeciesSettingDtoList[0].value
)"
:key="spec.speciesTypeBizId"
:prop="spec.speciesTypeBizId"
:label="spec.typeName"
align="center"
:width="form.speciesData.length > 5 ? 150 : 'auto'"
fixed="left"
>
<template #default="scope">
{{ scope.row[spec.speciesTypeBizId] }}
</template>
</el-table-column>
<!-- 价格列 -->
<el-table-column
prop="price"
label="价格"
:width="tableData.length > 0 ? 120 : 'auto'"
align="center"
>
<template #header>
<span class="required-label">价格</span>
</template>
<template #default="scope">
<el-input v-model="scope.row.price" type="number" />
<div class="errMsg" v-if="scope.row.priceErrorMsg">
{{ scope.row.priceErrorMsg }}
</div>
</template>
</el-table-column>
<!-- 库存列 -->
<el-table-column
prop="inventory"
label="库存"
:width="tableData.length > 0 ? 120 : 'auto'"
align="center"
>
<template #header>
<span class="required-label">库存</span>
</template>
<template #default="scope">
<el-input v-model="scope.row.inventory" type="number" />
<div class="errMsg" v-if="scope.row.inventoryErrorMsg">
{{ scope.row.inventoryErrorMsg }}
</div>
</template>
</el-table-column>
<!-- 配图列 -->
<el-table-column
prop="illustrationUrl"
label="配图"
:width="tableData.length > 0 ? 120 : 'auto'"
align="center"
>
<template #default="scope">
<div style="display: flex; align-items: center; justify-content: center">
<img
v-if="scope.row.illustrationUrl"
:src="scope.row.illustrationUrl"
style="width: 60%; height: 60%; object-fit: cover"
/>
<el-icon v-else :size="24" color="#969696">
<Picture />
</el-icon>
</div>
</template>
</el-table-column>
<!-- 状态列 -->
<el-table-column
prop="status"
label="状态"
:width="tableData.length > 0 ? 120 : 'auto'"
align="center"
fixed="right"
>
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="1" inactive-value="0" />
<span style="margin-left: 8px">
{{ scope.row.status === '1' ? '上架' : '下架' }}
</span>
</template>
</el-table-column>
</el-table>
</el-row>
</div>
</el-card>
<!-- 更多设置 -->
<el-card class="cardStyle">
<template #header>
<div>
<span class="cardTitle">更多设置</span>
</div>
</template>
<el-row>
<el-col :span="24">
<div class="moreBox">
<div class="moreTitle">定时开售</div>
<div class="moreItem">
<div class="moreLeft">设置定时</div>
<div class="moreRight">
<el-switch
v-model="form.apiProductLaunchDto['isTiming']"
active-value="1"
inactive-value="0"
style="margin: 0 5px 0px 15px"
></el-switch>
<span class="imgSwitch">{{
form.apiProductLaunchDto['isTiming'] == '1' ? '已开启' : '已关闭'
}}</span>
</div>
</div>
<div class="moreItem" v-if="form.apiProductLaunchDto['isTiming'] == '1'">
<div class="moreLeft">开售日期</div>
<div class="moreRight" style="margin-left: 15px">
<el-date-picker
v-model="form.apiProductLaunchDto['releaseDate']"
style="width: 100%"
type="datetime"
placeholder="请选择开售日期"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</div>
</div>
<div class="moreItem" v-if="form.apiProductLaunchDto['isTiming'] == '1'">
<div class="moreLeft">隐藏信息</div>
<div class="moreRight" style="margin-left: 15px">
<el-checkbox
v-model="form.apiProductLaunchDto['isHiddenPrice']"
label="价格"
size="large"
/>
</div>
</div>
</div>
<div class="moreBox">
<div class="moreTitle">购买设置</div>
<div class="moreItem">
<div class="moreLeft">
{{ form.apiProductLaunchDto['isPurchaseLimit'] == '1' ? '购买限制' : '限购' }}
</div>
<div class="moreRight">
<el-switch
v-model="form.apiProductLaunchDto['isPurchaseLimit']"
active-value="1"
inactive-value="0"
style="margin: 0 5px 0px 15px"
></el-switch>
<span class="imgSwitch">{{
form.apiProductLaunchDto['isPurchaseLimit'] == '1' ? '开启限购' : '未开启'
}}</span>
</div>
</div>
<div class="moreItem" v-if="form.apiProductLaunchDto['isPurchaseLimit'] == '1'">
<div class="moreLeft"></div>
<div class="moreRight" style="margin-left: 15px">
<span>每人</span>
<el-select
placeholder="请选择"
style="width: 100px; margin-right: 5px"
v-model="form.apiProductLaunchDto['limitDateUnit']"
>
<el-option
v-for="option in limitoptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<span>限购</span>
<el-input
v-model="form.apiProductLaunchDto['limitQuantity']"
placeholder="件数"
type="number"
style="width: 120px; margin-right: 20px"
>
<template #append>
<span></span>
</template>
</el-input>
</div>
</div>
</div>
<div class="moreBox">
<div class="moreTitle">上架平台</div>
<el-button
type="primary"
plain
@click="choosePlatform"
style="margin-top: 10px; margin-bottom: 10px"
>选择平台</el-button
>
<el-table :data="choosePlatformList" border>
<el-table-column
label="项目名称"
prop="projectName"
:show-overflow-tooltip="true"
/>
<el-table-column label="项目编号" prop="projectCode" />
<el-table-column
label="操作"
align="center"
class-name="small-padding fixed-width"
width="200"
fixed="right"
>
<template #default="scope">
<el-button
link
type="danger"
icon="Delete"
@click="deletePlatform(scope.row, scope.$index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</el-col>
</el-row>
</el-card>
</el-form>
</div>
<div class="bottomBtn">
<el-button type="primary" size="large" @click="submitProduct">提交</el-button>
</div>
<!-- 选择平台 -->
<el-dialog title="平台" v-model="platFormOpen" width="700px" append-to-body>
<el-form
:model="platFormQueryParams"
ref="platFormQueryRef"
:inline="true"
label-width="68px"
>
<el-form-item label="项目名称" prop="projectName">
<el-input
v-model="platFormQueryParams.projectName"
placeholder="请输入项目名称"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="searchPlatformTable">搜索</el-button>
<el-button icon="Refresh" @click="paltformReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格数据 -->
<el-table
v-loading="platFormLoading"
:data="platFormList"
@selection-change="platformSelectionChange"
:row-key="row => row.projectBizId"
:reserve-selection="true"
ref="platformTableRef"
>
<el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
<el-table-column label="项目名称" prop="projectName" />
<el-table-column label="项目编号" prop="projectCode" />
</el-table>
<pagination
v-show="platformTotal > 0"
:total="platformTotal"
v-model:page="platFormQueryParams.pageNo"
v-model:limit="platFormQueryParams.pageSize"
@pagination="getPlatFormList"
/>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="confirmPlatform">确 定</el-button>
<el-button @click="closePlatform">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 修改分类类目 -->
<el-dialog title="修改产品类目" v-model="categoryDialogOpen" width="1000px" append-to-body>
<CategoryTable
style="height: 300px"
type="edit"
ref="categoryRef"
:data="selectCategoryData"
/>
<template #footer>
<div class="categoryDialogFooter">
<el-button type="primary" @click="categoryConfirm">确 定</el-button>
<el-button @click="categoryDialogOpen = false">取 消</el-button>
</div>
</template>
</el-dialog>
<el-dialog
:title="`${errorList.length}处需修改`"
v-model="errorDialogOpen"
width="800px"
append-to-body
>
<div class="errorContent">
<div class="errorTitle">以下问题会导致提交不通过,建议修改完再提交</div>
<div class="errorItem" v-for="error in errorList">
<el-icon class="errMsg" :size="22" style="margin-right: 5px"><Warning /></el-icon>
<span>{{ error.msg }}</span>
</div>
</div>
<template #footer>
<div class="categoryDialogFooter">
<el-button type="primary" @click="errorDialogOpen = false">去修改</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="InsuranceProduct">
import { assign, cloneDeep } from 'lodash-es'
import {
getProductList,
addProduct,
getProductDetail,
productParams,
ParamRightOptions,
productSpecies,
querySelectCategory,
saveProductLaunch
} from '@/api/product/index'
import { listProject } from '@/api/system/project'
import { getToken } from '@/utils/auth'
import useUserStore from '@/store/modules/user'
import { ref, computed, watch, nextTick } from 'vue'
import ImageUpload from '@/components/ImageUpload/index.vue' //图片上传组件
import CategoryTable from '@/components/CategoryTable/index.vue' //图片上传组件
const emit = defineEmits(['handleSuccess'])
const props = defineProps({
// 类型,是新增还是编辑,
activeName: {
type: String,
default: ''
},
detailInfo: {
type: Object,
default: () => {}
}
})
const headers = ref({ Authorization: 'Bearer ' + getToken() })
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/oss/api/oss/upload')
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const showImage = ref(true)
const SummaryRightOptions = ref([]) //产品参数右侧下拉框汇总列表
const paramsList = ref([]) //产品参数总列表
const subImageInfo = ref({ count: 0, fileList: [] }) //详情图信息
const showNameTip = ref(false)
const imageInfo = ref({ count: 0, fileList: [] })
const productList = ref([])
const errorList = ref([]) //存储错误信息
const errorDialogOpen = ref(false)
const showOnce = ref(false)
const allProductList = ref([])
const nameRequireList = ref([
{
label: '标题中必须包含产品名称',
key: '1'
},
{
label: '标题禁止出现连续重复的关键词',
key: '2'
},
{
label: '标题字数不少于5个字,包括中文、 英文、数字',
key: '3'
},
{
label: '标题为非中文时需要附带与产品对应一致的中文翻译',
key: '4'
},
{
label: '标题中需避免填写错别字',
key: '5'
}
])
const imageRequireList = ref([
{
label: '商品主图须符合平台要求,否则将无法通过审核。',
key: '1',
showTip: true,
tipContent: '查看主图要求'
},
{
label: '最少上传3个素材,最多上传9个素材,其中最多上传1个视频(mp4格式),拖拽图片可排序。',
key: '2'
},
{
label: '视频时长不超过180秒,视频大小不超过500M;单张图片需限制在10M以内。',
key: '3'
}
])
const desRequireList = ref([
{
label: '商品详情图须符合平台要求,否则将无法通过审核。',
key: '1',
showTip: true,
tipContent: '查看详情图要求'
},
{
label: '至少上传1个素材,最多上传50个素材,拖拽可进行排序。',
key: '2'
},
{
label: '单张图片需限制在10M以内。',
key: '3'
}
])
const imageTipList = ref([
{
label: '全类目的产品主图要求提供不少于3张产品相关图片',
key: '1'
},
{
label: '主图第一张需为产品实物图',
key: '2'
},
{
label: '产品主图中不可包含相同的图片',
key: '3'
},
{
label: '产品图片不允许为手机截图、空白图',
key: '4'
},
{
label: '图片需清晰,不允许打码、遮挡/ps',
key: '5'
}
])
const desTipList = ref([
{
label: '商品详情图不得少于1张',
key: '1'
},
{
label: '详情图需为与商品相关的图片',
key: '2'
},
{
label: '商品详情图中不可包含相同的图片',
key: '3'
},
{
label: '商品图片不允许为手机截图、空白图',
key: '4'
},
{
label: '图片需清晰,不允许打码、遮挡/ps、第三方平台信息等',
key: '5'
}
])
const data = reactive({
form: {
apiProductLaunchDto: {},
apiAttributeSettingDtoList: [],
speciesData: [], //规格设置的数据
apiSpeciesPriceDtoList: []
},
queryParams: {},
platFormQueryParams: {
pageNo: 1,
pageSize: 10
}
})
const { queryParams, form, platFormQueryParams } = toRefs(data)
const handleDateChange = () => {}
// ===========更多设置开始===========
const choosePlatformList = ref([]) // 表格数据
const platFormOpen = ref(false)
const platFormLoading = ref(false)
const platFormList = ref([])
const platformSelectRows = ref([])
const platformTotal = ref(0)
const platformTableRef = ref()
const selectedRows = ref([])
// 使用 Set 来管理已选择的ID,方便查找
const selectedPlatformIds = ref(new Set())
const limitoptions = ref([
{
label: '每日',
value: 'DAILY',
key: '1'
},
{
label: '每周',
value: 'WEEKLY',
key: '2'
},
{
label: '每月',
value: 'MONTHLY',
key: '3'
},
{
label: '每年',
value: 'YEARLY',
key: '4'
}
])
const platformSelectionChange = rows => {
platformSelectRows.value = rows
// 实时更新ID集合
selectedPlatformIds.value = new Set(rows.map(item => item.projectBizId))
}
const paltformReset = () => {
platFormQueryParams.value = {
pageNo: 1,
pageSize: 10
}
getPlatFormList()
}
const closePlatform = () => {
platFormQueryParams.value = {
pageNo: 1,
pageSize: 10
}
platFormOpen.value = false
}
// 确定选择的平台
const confirmPlatform = () => {
choosePlatformList.value = cloneDeep(platformSelectRows.value)
// 更新已选择的ID集合
selectedPlatformIds.value = new Set(choosePlatformList.value.map(item => item.projectBizId))
closePlatform()
}
// 2. 修改弹窗打开函数
const choosePlatform = async () => {
platFormOpen.value = true
platFormQueryParams.value.pageNo = 1
// 获取列表数据
await getPlatFormList()
}
const getPlatFormList = () => {
platFormLoading.value = true
try {
listProject(platFormQueryParams.value).then(response => {
platFormList.value = response.data.records
platformTotal.value = response.data.total
// 数据加载后,设置选择状态
nextTick(() => {
if (platformTableRef.value) {
// 先清空所有选择
// platformTableRef.value.clearSelection()
platFormList.value.forEach(row => {
// 不可删除,删除会有bug
choosePlatformList.value.forEach(item => {
if (item.projectBizId == row.projectBizId) {
platformTableRef.value.toggleRowSelection(row, true)
}
})
const isSelected = selectedPlatformIds.value.has(row.projectBizId)
platformTableRef.value.toggleRowSelection(row, isSelected)
})
}
})
})
} catch (error) {
platFormList.value = []
} finally {
platFormLoading.value = false
}
}
const searchPlatformTable = () => {
platFormQueryParams.value.pageNo = 1
getPlatFormList()
}
const deletePlatform = (row, index) => {
proxy.$modal
.confirm('是否确认删除平台名称为"' + row.projectName + '"的数据项?')
.then(function () {
if (choosePlatformList.value.length > 0) {
choosePlatformList.value.splice(index, 1)
platformSelectRows.value.splice(index, 1)
// 从已选择的ID集合中移除
selectedPlatformIds.value.delete(row.projectBizId)
}
})
.catch(() => {})
}
// ===========更多设置结束===========
// ===========产品规格与库存相关操作开始===========
const tableData = ref([]) // 表格数据
const tableColumns = ref([]) // 表格列配置
const comboData = ref({}) // 存储每个规格组合的数据(价格、库存等)
const speciesList = ref([])
const speciesSelectList = ref([])
const isSettingSpeciesPrice = ref(false) //是否根据详情值回显规格价格的值
// 存储每个规格下拉框选中的值
const selectedValues = ref({})
// 价格和库存输入
const batchPrice = ref('')
const batchInventory = ref('')
/** 获取产品的所有规格 */
function getProductSpeciesInfo() {
productSpecies({ objectBizIdList: form.value.apiProductLaunchDto.categoryBizIdList }).then(
response => {
if (response.code == 200) {
// 应该做获取右侧的下拉框值了
speciesList.value = []
speciesList.value = response.data
let obj = {
speciesTypeBizId: `customSpecies${Date.now() + Math.floor(Math.random() * 1000)}`, //字段表唯一业务ID
isCustomize: '1', //是否自定义 0-否 1-是
typeName: '自定义规格'
}
speciesList.value.push(obj)
if (
props.detailInfo.apiSpeciesTypeDtoList &&
props.detailInfo.apiSpeciesTypeDtoList.length > 0
) {
form.value.speciesData = cloneDeep(props.detailInfo.apiSpeciesTypeDtoList)
// 回显规格数据,处理每一项规格的options和自定义规格
form.value.speciesData.forEach(item => {
if (item.isCustomize == '1') {
item.speciesTypeBizId = `customSpecies${
Date.now() + Math.floor(Math.random() * 1000)
}`
item.speciesName = obj.speciesTypeBizId
} else {
item.speciesName = item.speciesTypeBizId
}
let options = getResidueSpecies()
// 非自定义规格的options需要从speciesList中获取
speciesList.value.forEach(item3 => {
if (item3.speciesTypeBizId == item.speciesTypeBizId) {
options.unshift(item3)
item.typeCode = item3.typeCode
}
})
item.apiSpeciesSettingDtoList.forEach((item2, index2) => {
item2.speciesTypeBizId = item.speciesTypeBizId
item2.speciesId = item2.speciesSettingBizId
item2.typeCode = item.typeCode
if (index2 == item.apiSpeciesSettingDtoList.length - 1) {
item2.isAdd = true
} else {
item2.isAdd = false
}
})
item.options = options
})
}
} else {
proxy.$modal.msgError(response.msg)
}
}
)
}
// 获取剩余的规格
function getResidueSpecies() {
const remainData = speciesList.value.filter(species => {
// 检查当前规格是否已经在 form.value.speciesList 中
return !form.value.speciesData.some(item => item.speciesTypeBizId === species.speciesTypeBizId)
})
return remainData
}
// 添加规格
function addSpecies() {
const residueSpecies = cloneDeep(getResidueSpecies())
if (residueSpecies.length > 1) {
const newSpecies = residueSpecies[0]
let speciesId = Date.now() + Math.floor(Math.random() * 1000)
newSpecies.speciesName = newSpecies.speciesTypeBizId
newSpecies.options = cloneDeep(residueSpecies)
newSpecies.isCustomize = '0'
//因为只有第一个规格需要添加图片,所以这里先判断一下,如果长度为0,则添加图片
if (form.value.speciesData.length == 0) {
newSpecies.isIllustration = '1'
newSpecies.illustrationUrl = ''
} else {
newSpecies.isIllustration = '0'
newSpecies.illustrationUrl = ''
}
newSpecies.apiSpeciesSettingDtoList = [
{
isIllustration: newSpecies.isIllustration,
speciesName: newSpecies.speciesName,
speciesTypeBizId: newSpecies.speciesTypeBizId,
typeName: newSpecies.typeName,
speciesId: speciesId,
isAdd: true,
isCustomize: '0'
}
]
form.value.speciesData.push(newSpecies)
} else {
// 如果只剩下一个规格,则直接添加自定义规格
let newObj = {
speciesTypeBizId: `customSpecies${Date.now() + Math.floor(Math.random() * 1000)}`, //字段表唯一业务ID
isCustomize: '1', //是否自定义 0-否 1-是
typeName: ''
}
if (form.value.speciesData.length == 0) {
newObj.isIllustration = '1'
} else {
newObj.isIllustration = '0'
}
newObj.apiSpeciesSettingDtoList = [
{
...newObj,
speciesId: Date.now() + Math.floor(Math.random() * 1000),
isAdd: true
}
]
form.value.speciesData.push(newObj)
}
// 由于新增的时候,规格值会重置,所以需要更新规格下拉框的值
getNewSpeciesOptions()
}
// 处理规格值得宽度
function getSpeciesMiddleWidth(species, item) {
if (species.isIllustration == '1' && item.apiSpeciesSettingDtoList.length < 2) {
// 有配图,但规格数量只有一个
return '92%'
} else if (species.isIllustration == '0' && item.apiSpeciesSettingDtoList.length > 1) {
// 无配图,但规格数量有多个
return '92%'
} else if (species.isIllustration == '0' && item.apiSpeciesSettingDtoList.length == 1) {
// 无配图,但规格数量只有一个
return '100%'
} else if (species.isIllustration == '1' && item.apiSpeciesSettingDtoList.length > 1) {
// 有配图,但规格数量有多个
return '84%'
} else {
return '100%'
}
}
// 规格上传图片前得校验
function handleBeforeUpload(file) {
const isLt = file.size / 1024 / 1024 < 2
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 2 MB!`)
return false
}
return true
}
// 规格上传图片成功
const handleUploadSuccess = (res, file, species) => {
if (res.code == 200) {
form.value.speciesData.forEach(item => {
if (item.speciesTypeBizId == species.speciesTypeBizId) {
item.apiSpeciesSettingDtoList.forEach(item2 => {
if (item2.speciesId == species.speciesId) {
item2.illustrationUrl = res.data.url
}
})
}
})
}
}
// 添加规格值
const speciesInputChange = (species, sIndex) => {
let speciesObj = { ...species }
// 添加规格值
let speciesId = Date.now() + Math.floor(Math.random() * 1000)
let newSpeciesValue = {
speciesId: speciesId,
speciesTypeBizId: speciesObj.speciesTypeBizId,
isAdd: true,
value: '',
typeName: speciesObj.typeName,
isCustomize: speciesObj.isCustomize
}
if (species.isIllustration == '1') {
newSpeciesValue.isIllustration = '1'
newSpeciesValue.illustrationUrl = ''
} else {
newSpeciesValue.isIllustration = '0'
newSpeciesValue.illustrationUrl = ''
}
form.value.speciesData.forEach((item, index) => {
if (item.speciesTypeBizId == speciesObj.speciesTypeBizId) {
item.apiSpeciesSettingDtoList.forEach((item2, i) => {
if (item2.speciesId == speciesObj.speciesId && item2.isAdd) {
item.apiSpeciesSettingDtoList.push(newSpeciesValue)
item2.isAdd = false
}
})
}
})
}
// 配图开关切换
const switchBeforeChange = obj => {
return false
}
// 配图开关
const switchChange = (obj, type) => {
form.value.speciesData.forEach((item, index) => {
if (item.speciesTypeBizId == obj.speciesTypeBizId) {
if (type == 'close') {
if (obj.isIllustration == '0') {
item.isIllustration = '1'
} else if (obj.isIllustration == '1') {
item.isIllustration = '0'
}
} else {
item.isIllustration = obj.isIllustration
}
item.apiSpeciesSettingDtoList.forEach((item2, i) => {
item2.isIllustration = item.isIllustration
if (item.isIllustration == '0') {
item2.illustrationUrl = ''
}
})
}
})
}
// 规格下拉框切换
const changeSpeciesSelect = (obj, index) => {
//自定义规格只是把当前的规格给替换掉,并不是新增,写到这里了
let slefObj = { ...obj }
let newForm = { ...form.value }
const sameItem = speciesList.value.filter(item => {
if (item.speciesTypeBizId == slefObj.speciesName) {
return item
}
})
//更新form里对应的数据
newForm.speciesData.forEach((item, i) => {
let newSpecies = {}
if (item.speciesTypeBizId == slefObj.speciesTypeBizId) {
if (slefObj.speciesName.includes('customSpecies')) {
// 如果是自定义的要把自定义的数据替换成新的,
item.speciesTypeBizId = `customSpecies${Date.now() + Math.floor(Math.random() * 1000)}`
item.typeName = ''
item.isCustomize = '1'
newSpecies = { ...item }
} else {
item.isCustomize = '0'
item.speciesTypeBizId = slefObj.speciesName
item.typeName = sameItem[0].typeName
newSpecies = { ...sameItem[0] }
}
if (index == 0) {
newSpecies.isIllustration = '1'
} else {
newSpecies.isIllustration = '0'
}
item.apiSpeciesSettingDtoList = [
{ ...newSpecies, speciesId: Date.now() + Math.floor(Math.random() * 1000), isAdd: true }
]
}
})
form.value = newForm
// 由于规格下拉框切换的时候,规格值会重置,所以需要更新规格下拉框的值
getNewSpeciesOptions()
}
// 得到最新的规格下拉框数据
const getNewSpeciesOptions = () => {
const remainSpeciesOptins = getResidueSpecies()
if (remainSpeciesOptins.length > 0) {
form.value.speciesData.forEach(item => {
if (item.options) {
let newOptions = cloneDeep(remainSpeciesOptins)
item.options.forEach(option => {
if (item.speciesTypeBizId == option.speciesTypeBizId) {
newOptions.unshift(option)
}
})
item.options = newOptions
} else {
item.speciesName = remainSpeciesOptins[0].speciesTypeBizId
item.options = remainSpeciesOptins
}
})
}
}
// 改变规格自定义名字得时候,规格值得typeName要同步
const changCustomSpecies = obj => {
let newObj = { ...obj }
// 自定义规格的时候要看一下有没有重复的,如果有重复的就加错误信息,没有重复的就清空错误信息
let repeat = form.value.speciesData.filter(item => item.typeName == newObj.typeName)
form.value.speciesData.forEach((item, index) => {
if (item.speciesTypeBizId == newObj.speciesTypeBizId) {
if (repeat.length > 1) {
item.errorMsg = '规格名称不能重复'
} else if (!newObj.typeName && item.apiSpeciesSettingDtoList[0].value) {
item.errorMsg = '规格名称不能为空'
} else {
item.errorMsg = ''
}
item.apiSpeciesSettingDtoList.forEach((item2, i) => {
item2.typeName = newObj.typeName
})
}
})
}
// 删除规格
const deleteSpecies = obj => {
let slefObj = { ...obj }
form.value.speciesData.forEach((item, index) => {
if (item.speciesTypeBizId == slefObj.speciesTypeBizId) {
form.value.speciesData.splice(index, 1)
}
})
if (form.value.speciesData.length > 0) {
form.value.speciesData[0].isIllustration = '1'
form.value.speciesData[0].apiSpeciesSettingDtoList.forEach((item2, i) => {
item2.isIllustration = '1'
})
}
}
// 删除规格值
const deleteSpeciesValue = obj => {
let slefObj = { ...obj }
form.value.speciesData.forEach((item, index) => {
if (item.speciesTypeBizId == slefObj.speciesTypeBizId) {
item.apiSpeciesSettingDtoList.forEach((item2, i2) => {
if (item2.speciesId == slefObj.speciesId) {
item.apiSpeciesSettingDtoList.splice(i2, 1)
}
})
}
})
}
//规格设置下拉框
const getSpeciesSelect = () => {
speciesSelectList.value = []
let newArray = cloneDeep(form.value.speciesData)
newArray.forEach(item => {
if (item.typeName && item.apiSpeciesSettingDtoList[0].value) {
let newOptions = item.apiSpeciesSettingDtoList.filter(item2 => {
return item2.value
})
newOptions.unshift({
value: `全部${item.typeName}`,
speciesId: item.speciesTypeBizId,
type: 'all'
})
item.options = newOptions
speciesSelectList.value.push(item)
}
})
}
// 初始化选中状态,默认选中"全部"
const initSelectedValues = () => {
speciesSelectList.value.forEach(item => {
// 找到"全部"选项,类型为'all'
const allOption = item.options.find(opt => opt.type === 'all')
if (allOption) {
selectedValues.value[item.speciesTypeBizId] = allOption.speciesId
}
})
}
// 批量设置价格和库存
const handleBatchSet = () => {
if (!batchPrice.value && !batchInventory.value) {
proxy.$modal.msgError('请输入价格或库存')
return
}
// 获取当前选中的规格值
const currentSelections = {}
speciesSelectList.value.forEach(item => {
const speciesTypeBizId = item.speciesTypeBizId
const selectedValue = selectedValues.value[speciesTypeBizId]
currentSelections[speciesTypeBizId] = selectedValue
})
// 过滤需要更新的表格行
const filteredRows = tableData.value.filter(row => {
// 检查每一行是否满足所有规格条件
return Object.keys(currentSelections).every(speciesTypeBizId => {
const selectedValue = currentSelections[speciesTypeBizId]
// 如果选择的是"全部"(speciesId等于speciesTypeBizId)
if (selectedValue === speciesTypeBizId) {
// "全部"意味着匹配该规格的所有值
return true
}
// 否则检查具体值是否匹配
// 通过_id字段来匹配
const rowSpeciesId = row[`${speciesTypeBizId}_id`]
return rowSpeciesId === selectedValue
})
})
// 更新匹配的行
filteredRows.forEach(row => {
if (batchPrice.value !== '') {
row.price = Number(batchPrice.value)
}
if (batchInventory.value !== '') {
row.inventory = Number(batchInventory.value)
}
})
tableData.value.forEach(item => {
if (!item.price) {
item.priceErrorMsg = '请输入价格'
} else {
item.priceErrorMsg = ''
}
if (!item.inventory) {
item.inventoryErrorMsg = '请输入库存'
} else {
item.inventoryErrorMsg = ''
}
})
}
// 在生成speciesSelectList后调用
watch(
() => speciesSelectList.value,
() => {
if (speciesSelectList.value.length > 0) {
initSelectedValues()
}
},
{ immediate: true, deep: true }
)
// 生成所有规格组合(笛卡尔积)
const generateAllCombinations = specs => {
if (specs.length === 0) return []
const result = []
const firstSpec = specs[0]
const restSpecs = specs.slice(1)
// 如果还有剩余的规格,递归生成剩余规格的组合
const restCombinations = restSpecs.length > 0 ? generateAllCombinations(restSpecs) : [{}]
// 遍历第一个规格的每个值
for (let valueObj of firstSpec.apiSpeciesSettingDtoList) {
// 跳过空值(新增的空白项)
if (valueObj.value === '') continue
for (let restComb of restCombinations) {
let row = {
// 存储每个规格的值和ID
[firstSpec.speciesTypeBizId]: valueObj.value,
[`${firstSpec.speciesTypeBizId}_id`]: valueObj.speciesId,
speciesId: valueObj.speciesId,
speciesTypeBizId: firstSpec.speciesTypeBizId,
// 存储规格信息用于合并计算
_specs: {
[firstSpec.speciesTypeBizId]: {
value: valueObj.value,
typeName: firstSpec.typeName,
isIllustration: firstSpec.isIllustration,
speciesId: firstSpec.speciesId,
speciesTypeBizId: firstSpec.speciesTypeBizId,
isCustomize: firstSpec.isCustomize
}
}
}
// 添加剩余规格的值
for (let spec of restSpecs) {
row[spec.speciesTypeBizId] = restComb[spec.speciesTypeBizId]
row[`${spec.speciesTypeBizId}_id`] = restComb[`${spec.speciesTypeBizId}_id`]
row._specs[spec.speciesTypeBizId] = {
value: restComb[spec.speciesTypeBizId],
typeName: spec.typeName,
isIllustration: spec.isIllustration,
illustrationUrl: spec.illustrationUrl || '',
isCustomize: spec.isCustomize,
speciesTypeBizId: spec.speciesTypeBizId
}
if (!spec.speciesTypeBizId.includes('customSpecies')) {
row._specs[spec.speciesTypeBizId].speciesTypeBizId = spec.speciesTypeBizId
}
}
// 生成组合ID
const comboId = specs.map(spec => row[`${spec.speciesTypeBizId}_id`]).join('_')
row.comboId = comboId
// 如果有存储的数据,使用存储的数据
if (comboData.value[comboId]) {
row = { ...row, ...comboData.value[comboId] }
} else {
// 默认值
row.price = 0
row.inventory = 0
row.status = '1' // 1:上架, 0:下架
row.illustrationUrl = ''
}
if (valueObj.isIllustration && valueObj.isIllustration == '1') {
row.isIllustration = valueObj.isIllustration
}
if (valueObj.illustrationUrl) {
row.illustrationUrl = valueObj.illustrationUrl
}
if (valueObj.speciesId) {
row.speciesId = valueObj.speciesId
}
result.push(row)
}
}
return result
}
// 生成表格列配置
const generateTableColumns = specs => {
const columns = []
// 动态规格列
specs.forEach(spec => {
if (spec.typeName) {
columns.push({
prop: spec.speciesTypeBizId,
label: spec.typeName,
isSpec: true
})
}
})
return columns
}
// 计算合并单元格
const computeSpans = (data, specs) => {
const spans = []
let pos = 0
// 初始化
for (let i = 0; i < data.length; i++) {
spans[i] = []
for (let j = 0; j < specs.length; j++) {
spans[i][j] = { rowspan: 1, colspan: 1 }
}
}
// 对每个规格列进行处理
for (let j = 0; j < specs.length; j++) {
const specId = specs[j].speciesTypeBizId
let startRow = 0
for (let i = 1; i <= data.length; i++) {
// 需要比较当前行和上一行的值,以及前面规格的值
let same = true
for (let k = 0; k <= j; k++) {
const prevSpecId = specs[k].speciesTypeBizId
if (i < data.length && data[i][prevSpecId] !== data[i - 1][prevSpecId]) {
same = false
break
}
}
if (i === data.length || !same) {
// 从startRow到i-1行合并
for (let k = startRow; k < i; k++) {
if (k === startRow) {
spans[k][j].rowspan = i - startRow
} else {
spans[k][j].rowspan = 0
spans[k][j].colspan = 0
}
}
startRow = i
}
}
}
return spans
}
// 合并单元格方法
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
let newArray = cloneDeep(form.value.speciesData)
const specs = newArray.filter(item => item.typeName && item.apiSpeciesSettingDtoList[0].value)
if (columnIndex < specs.length && spans.value[rowIndex]) {
return [spans.value[rowIndex][columnIndex].rowspan, spans.value[rowIndex][columnIndex].colspan]
}
return [1, 1]
}
const spans = ref([])
// 生成表格数据
const generateTable = () => {
tableColumns.value = []
let newArray = cloneDeep(form.value.speciesData)
const specs = newArray.filter(item => item.typeName && item.apiSpeciesSettingDtoList[0].value)
// 生成列
tableColumns.value = generateTableColumns(specs)
// 生成行数据
tableData.value = generateAllCombinations(specs)
// 计算合并单元格
spans.value = computeSpans(tableData.value, specs)
if (isSettingSpeciesPrice.value) {
// 刚进来的时候,要根据详情中的apiSpeciesPriceDtoList去更新tableData中的价格和库存
let apiSpeciesPriceDtoList = cloneDeep(props.detailInfo.apiSpeciesPriceDtoList)
tableData.value.forEach((item1, index1) => {
apiSpeciesPriceDtoList.forEach((item2, index2) => {
if (index1 == index2) {
item1.status = String(item2.status)
item1.illustrationUrl = item2.illustrationUrl
item1.inventory = item2.inventory
item1.price = item2.price
}
})
})
isSettingSpeciesPrice.value = false
}
}
// 监听规格数据变化
watch(
() => form.value.speciesData,
(newVal, oldVal) => {
// 更新规格表格数据
generateTable()
//设置规格设置下拉框
getSpeciesSelect()
},
{ deep: true }
)
// ===========产品规格与库存相关操作结束===========
// ===========产品参数相关操作开始===========
const deleteParams = (item, index) => {
form.value.apiAttributeSettingDtoList.splice(index, 1)
// 还要处理每一项的leftOptions
getNewLeftOptions()
}
const deleteCustomParams = (item, index) => {
form.value.apiAttributeSettingDtoList.splice(index, 1)
}
const addParams = changeItem => {
let changeParam = { ...changeItem }
// 不要随意切换代码位置,会出现时机问题,会出现bug
// 正确的筛选逻辑:过滤出 form.value.apiAttributeSettingDtoList 中不存在的参数
const remainParams = getRemainData()
// 因为最后一个是自定义参数,所以加参加到最后一个直接走自定义参数的逻辑
if (!(changeParam.name && changeParam.name.includes('custom')) && remainParams.length > 1) {
// 按照 paramsList.value 的顺序,只添加第一个未添加的参数
form.value.apiAttributeSettingDtoList.push({
...remainParams[0],
isCustomize: '0',
leftOptions: remainParams, // 所有剩余参数作为 leftOptions
name: remainParams[0].fieldBizId
})
// 还要处理每一项的leftOptions
getNewLeftOptions()
} else {
let leftOptions = getRemainData()
// 因为这里添加自定义参数的时候,需要把之前的自定义参数先删除,在增加新的自定义参数,这样左侧的下拉框中的才能对应上
leftOptions.splice(leftOptions.length - 1, 1)
let fieldBizId = `custom${Date.now() + Math.floor(Math.random() * 1000)}`
// 如果没有剩余参数,则添加自定义参数
const customParam = {
productLaunchBizId: route.query.productLaunchBizId, //产品上架信息表唯一业务ID
fieldBizId: fieldBizId, //字段表唯一业务ID
fieldValueBizId: '', //字段值表唯一业务ID
name: '', //字段名称
value: '', //字段值
isCustomize: '1', //是否自定义 0-否 1-是
fieldCnName: '自定义参数',
customValue: fieldBizId //自定义值
}
leftOptions.push(customParam)
customParam.leftOptions = leftOptions
form.value.apiAttributeSettingDtoList.push(customParam)
// 还要处理每一项的leftOptions,自定义的参数不需要处理,先暂时不处理,先放在这,需要处理的
// let remainLeftOptins = getRemainData().map(item => {
// if (!item.isCustomize) return item
// })
// remainLeftOptins.push(customParam)
// if (remainLeftOptins.length > 0) {
// form.value.apiAttributeSettingDtoList.forEach(item => {
// if (item.leftOptions && !item.isCustomize) {
// let newOptions = JSON.parse(JSON.stringify(remainLeftOptins))
// item.leftOptions.forEach(option => {
// if (item.fieldBizId == option.fieldBizId) {
// newOptions.unshift(option)
// }
// })
// item.leftOptions = newOptions
// }
// })
// }
}
// 处理必填提示
paramsErr()
}
// 给产品参数加必填提示
const paramsErr = () => {
let errorList = []
form.value.apiAttributeSettingDtoList.forEach(item => {
if (item.isCustomize == '0' && item.value.length == 0) {
item.fieldCnName = item.leftOptions[0].fieldCnName
item.errorMsg = `参数${item.fieldCnName}为必填项`
errorList.push({ msg: item.errorMsg })
} else if (item.isCustomize == '1') {
if (!item.name && !item.value) {
item.errorMsg = `参数名称不能为空,参数值不能为空`
errorList.push({ msg: item.errorMsg })
} else if (!item.name) {
item.errorMsg = `参数名称不能为空`
errorList.push({ msg: item.errorMsg })
} else if (!item.value) {
item.errorMsg = `参数值不能为空`
errorList.push({ msg: item.errorMsg })
} else {
item.errorMsg = ``
}
} else {
item.errorMsg = ``
}
})
return errorList
}
const getRemainData = () => {
const remainData = paramsList.value.filter(param => {
// 检查当前参数是否已经在 form.value.apiAttributeSettingDtoList 中
return !form.value.apiAttributeSettingDtoList.some(item => item.fieldBizId === param.fieldBizId)
})
return remainData
}
const getNewLeftOptions = () => {
const remainLeftOptins = getRemainData()
if (remainLeftOptins.length > 0) {
form.value.apiAttributeSettingDtoList.forEach(item => {
if (item.leftOptions) {
let newOptions = cloneDeep(remainLeftOptins)
item.leftOptions.forEach(option => {
if (item.fieldBizId == option.fieldBizId) {
newOptions.unshift(option)
}
})
item.leftOptions = newOptions
} else {
let newOptions = cloneDeep(remainLeftOptins)
let option = cloneDeep(paramsList.value).filter(
item2 => item.isCustomize == '0' && item2.fieldBizId == item.fieldBizId
)
if (option.length > 0) {
newOptions.unshift(option[0])
}
if (item.isCustomize == '1') {
item.customValue = '自定义参数'
}
item.leftOptions = newOptions
}
})
}
}
const changeLeft = (changeItem, index) => {
const currentItem = cloneDeep(changeItem)
//如果选择的是自定义参数,则走添加参数的流程
if (currentItem.name.includes('custom')) {
// 先删除掉form中的当前项
if (form.value.apiAttributeSettingDtoList.length > 0) {
form.value.apiAttributeSettingDtoList.splice(index, 1)
}
// 再添加自定义参数
addParams(currentItem)
return
}
// return
// 非自定义参数,切换下拉框的值
// 1.先在 paramsList.value 中查找是否有相同的项
const identicalItem = paramsList.value.find(item => item.fieldBizId === currentItem.name) //选择的当前项
// 判断form中fieldBizId和changeItem.fieldBizId,一致就更新掉form中的当前项的数据
form.value.apiAttributeSettingDtoList.forEach(item => {
if (item.fieldBizId == currentItem.fieldBizId) {
item.name = currentItem.name
for (const key in identicalItem) {
item[key] = identicalItem[key]
}
}
})
getNewLeftOptions()
}
const changeCustomLeft = (item, index) => {
const currentItem = { ...item }
//如果选择的是自定义参数,则走添加参数的流程
if (currentItem.customValue.includes('custom')) {
return
} else {
// 先删除掉form中的当前项
if (form.value.apiAttributeSettingDtoList.length > 0) {
form.value.apiAttributeSettingDtoList.splice(index, 1)
}
// 依据当前选择的customValue,去paramsList中查找对应的项再添加到form中
const identicalItem = paramsList.value.find(item => item.fieldBizId === currentItem.customValue) //选择的当前项
identicalItem.name = currentItem.customValue
identicalItem.leftOptions = getRemainData()
form.value.apiAttributeSettingDtoList.push(identicalItem)
// 还要处理每一项的leftOptions
getNewLeftOptions()
}
}
/** 获取产品参数信息 */
function getProductParamsInfo() {
productParams({ objectBizIdList: form.value.apiProductLaunchDto.categoryBizIdList }).then(
response => {
if (response.code == 200) {
// 应该做获取右侧的下拉框值了
paramsList.value = []
form.value.apiAttributeSettingDtoList = []
paramsList.value = response.data
const fieldBizIdList = response.data.map(item => item.fieldBizId)
if (paramsList.value.length > 0) {
paramsList.value.forEach(item => {
item.isCustomize = '0'
})
}
paramsList.value.push({
productLaunchBizId: route.query.productLaunchBizId, //产品上架信息表唯一业务ID
fieldBizId: `custom${Date.now() + Math.floor(Math.random() * 1000)}`, //字段表唯一业务ID
fieldValueBizId: '', //字段值表唯一业务ID
name: '', //字段名称
value: '', //字段值
isCustomize: '1', //是否自定义 0-否 1-是
fieldCnName: '自定义参数'
})
// 赋值
if (props.detailInfo.apiAttributeSettingDtoList.length == 0) {
if (paramsList.value.length == 0) {
form.value.apiAttributeSettingDtoList.push({
...paramsList.value[0],
leftOptions: paramsList.value,
name: '',
customValue: paramsList.value[0].fieldBizId
})
}
// else {
// form.value.apiAttributeSettingDtoList.push({
// ...paramsList.value[0],
// leftOptions: paramsList.value,
// name: paramsList.value[0].fieldBizId
// })
// }
} else {
// 回显的时候没有leftOptions和middleOptions,需要赋值
form.value.apiAttributeSettingDtoList = cloneDeep(
props.detailInfo.apiAttributeSettingDtoList
)
form.value.apiAttributeSettingDtoList.forEach(item => {
if (item.isCustomize == '0' && item.textBoxType !== 'multipleSelect') {
item.name = item.fieldBizId
if (item.fieldValueBizId) {
item.value = item.fieldValueBizId
}
}
if (item.textBoxType == 'multipleSelect') {
item.value = item.value.split(';')
}
})
}
getNewLeftOptions()
RightParamsOptionList(fieldBizIdList)
} else {
proxy.$modal.msgError(response.msg)
}
}
)
}
/** 获取产品参数右侧汇总options */
function RightParamsOptionList(fieldBizIdList) {
ParamRightOptions({ fieldBizIdList }).then(response => {
if (response.code == 200) {
SummaryRightOptions.value = response.data
paramsList.value.forEach(item => {
SummaryRightOptions.value.forEach(item2 => {
if (item.fieldBizId == item2.fieldBizId) {
item.middleOptions = item2.fieldValueListResponseList
}
})
})
form.value.apiAttributeSettingDtoList.forEach(item => {
SummaryRightOptions.value.forEach(item2 => {
if (item.fieldBizId == item2.fieldBizId) {
item.middleOptions = item2.fieldValueListResponseList
}
})
})
} else {
proxy.$modal.msgError(response.msg)
}
})
}
// ===========产品参数相关操作结束===========
const handleFileChange = info => {
if (info.source == 'main') {
imageInfo.value = cloneDeep(info)
form.value['apiProductLaunchDto'].mainUrlsList = info.fileList.map(item => item.url)
} else if (info.source == 'sub') {
subImageInfo.value = cloneDeep(info)
form.value['apiProductLaunchDto'].detailUrlsList = info.fileList.map(item => item.url)
}
}
const handleSelectChange = () => {
if (
form.value['apiProductLaunchDto'].title &&
form.value['apiProductLaunchDto'].title.length < 5
) {
showNameTip.value = true
} else if (!form.value['apiProductLaunchDto'].title) {
showNameTip.value = true
} else {
showNameTip.value = false
}
}
/** 查询产品列表 */
function searchProduct(query) {
let productName = query ? query.trim() : ''
let data = {
productName: productName,
pageSize: showOnce.value ? 9999 : 10,
pageNo: 1
}
getProductList(data).then(response => {
if (response.code === 200) {
productList.value = response.data.records
if (showOnce.value) {
allProductList.value = response.data.records
showOnce.value = false
console.log('allProductList.value', allProductList.value)
}
} else {
productList.value = []
proxy.$modal.msgError(response.msg)
}
})
}
// ===========基础信息相关操作开始===========
const selectCategoryData = ref([]) //已经选择的分类数据
const categoryDialogOpen = ref(false) //已经选择的分类数据
const categoryRef = ref(null)
// 选择的分类名称
const categoryName = computed(() => {
let result = []
selectCategoryData.value.forEach(item => {
item.data.forEach(c => {
if (c.isSelected) result.push(c.name)
})
})
if (result.length > 0) {
return result.join(' > ')
} else {
return ''
}
})
/** 查询已经选中的分类列表 */
function getSelectCategory() {
let data = {
objectBizId: form.value['apiProductLaunchDto'].productLaunchBizId,
type: 'PRODUCT'
}
querySelectCategory(data).then(response => {
if (response.code === 200) {
response.data = response.data.map(item => {
item.data = item.selectedResponseList
return item
})
selectCategoryData.value = response.data
} else {
selectCategoryData.value = []
proxy.$modal.msgError(response.msg)
}
})
}
const categoryConfirm = () => {
selectCategoryData.value = categoryRef.value.categoryList
categoryDialogOpen.value = false
}
// ===========基础信息相关操作结束===========
/** 上架产品 保存接口*/
function submitProduct() {
let submitObj = {}
errorList.value = []
// 产品
// 类目信息form.value['apiProductLaunchDto'].categoryBizIdList对应的是 selectCategoryData
if (!form.value['apiProductLaunchDto'].title) {
errorList.value.push({ msg: '标题信息过少,请至少输入5个有效字数(含中文、英文、数字)' })
}
// 处理图片
if (
!form.value['apiProductLaunchDto'].mainUrlsList ||
form.value['apiProductLaunchDto'].mainUrlsList.length == 0
) {
errorList.value.push({ msg: '请上传至少三张产品主图' })
}
if (
!form.value['apiProductLaunchDto'].detailUrlsList ||
form.value['apiProductLaunchDto'].detailUrlsList.length == 0
) {
errorList.value.push({ msg: '请上传至少一张产品详情图' })
}
//处理产品参数
let paramsErrors = paramsErr()
if (paramsErrors.length > 0) {
errorList.value.push(...paramsErrors)
}
// 处理产品参数
let apiAttributeSettingDtoList = cloneDeep(form.value.apiAttributeSettingDtoList)
apiAttributeSettingDtoList.forEach(item => {
if (!item.isCustomize) {
item.isCustomize = '0'
}
if (item.leftOptions) {
item.leftOptions.forEach(item2 => {
if (item.name == item2.fieldBizId) {
item.name = item2.fieldCnName
}
})
}
if (item.middleOptions) {
item.middleOptions.forEach(item2 => {
if (item.value == item2.fieldValueBizId) {
item.fieldValueBizId = item2.fieldValueBizId
item.value = item2.value
}
})
}
if (item.textBoxType == 'multipleSelect') {
item.value = item.value.join(';')
}
item.productLaunchBizId = form.value['apiProductLaunchDto'].productLaunchBizId
delete item.leftOptions
delete item.middleOptions
})
// 处理产品规格和库存
// 处理规格列表
let apiSpeciesSettingDtoList = []
let newData = cloneDeep(form.value.speciesData)
if (newData.length == 0) {
proxy.$modal.msgError('请添加产品规格')
return
}
newData.forEach(item => {
if (item.errorMsg) {
errorList.value.push({ msg: item.errorMsg })
}
item.apiSpeciesSettingDtoList.forEach(item2 => {
item2.productLaunchBizId = form.value['apiProductLaunchDto'].productLaunchBizId
if (item2.isCustomize == '1' && item2.value) {
apiSpeciesSettingDtoList.push({
typeName: item2.typeName,
value: item2.value,
isIllustration: item2.isIllustration,
illustrationUrl: item2.illustrationUrl,
isCustomize: item2.isCustomize,
productLaunchBizId: form.value['apiProductLaunchDto'].productLaunchBizId
})
} else if (item2.isCustomize == '0' && item2.value) {
apiSpeciesSettingDtoList.push(item2)
}
})
})
// 处理价格库存列表
let apiSpeciesPriceDtoList = []
tableData.value.forEach(item => {
if (!item.price) {
item.priceErrorMsg = '请输入价格'
} else {
item.priceErrorMsg = ''
}
if (!item.inventory) {
item.inventoryErrorMsg = '请输入库存'
} else {
item.inventoryErrorMsg = ''
}
if (!item.price || !item.inventory) {
errorList.value.push({ msg: item.priceErrorMsg || item.inventoryErrorMsg })
}
})
let newPrice = cloneDeep(tableData.value)
newPrice.forEach(item => {
item.apiSpeciesSettingDtoList = []
for (let key in item._specs) {
speciesList.value.forEach(spec => {
if (spec.speciesTypeBizId == key) {
item._specs[key].typeCode = spec.typeCode
}
})
item.apiSpeciesSettingDtoList.push(item._specs[key])
}
apiSpeciesPriceDtoList.push({
productLaunchBizId: form.value['apiProductLaunchDto'].productLaunchBizId,
price: item.price, //价格
inventory: item.inventory, //库存
illustrationUrl: item.illustrationUrl, //配图url
status: item.status, //状态:0=下架,1=上架
apiSpeciesSettingDtoList: item.apiSpeciesSettingDtoList
})
})
if (errorList.value.length > 0) {
errorDialogOpen.value = true
return
} else {
errorDialogOpen.value = false
}
// 处理类目数据
form.value['apiProductLaunchDto'].categoryBizIdList = []
selectCategoryData.value.forEach(item => {
if (item.data) {
item.data.forEach(item2 => {
if (item2.isSelected) {
form.value['apiProductLaunchDto'].categoryBizIdList.push(item2.categoryBizId)
}
})
}
})
// 处理平台数据
let projectBizIdList = []
choosePlatformList.value.forEach(item => {
projectBizIdList.push(item.projectBizId)
})
form.value['apiProductLaunchDto'].projectBizIdList = projectBizIdList
// 处理定期开售
if (form.value['apiProductLaunchDto'].isTiming == '0') {
form.value['apiProductLaunchDto'].releaseDate = ''
form.value['apiProductLaunchDto'].isHiddenPrice = ''
} else if (form.value['apiProductLaunchDto'].isTiming == '1') {
form.value['apiProductLaunchDto'].isHiddenPrice = form.value['apiProductLaunchDto']
.isHiddenPrice
? '1'
: '0'
}
// 处理限购
if (form.value['apiProductLaunchDto'].isPurchaseLimit == '0') {
form.value['apiProductLaunchDto'].limitDateUnit = ''
form.value['apiProductLaunchDto'].limitQuantity = ''
}
submitObj.apiAttributeSettingDtoList = apiAttributeSettingDtoList
submitObj.apiSpeciesSettingDtoList = apiSpeciesSettingDtoList
submitObj.apiSpeciesPriceDtoList = apiSpeciesPriceDtoList
submitObj.apiProductLaunchDto = form.value['apiProductLaunchDto']
if (route.query.source && route.query.source == 'copy') {
submitObj.source = 1
} else {
submitObj.source = null
}
if (form.value['apiProductLaunchDto'].title) {
allProductList.value.forEach(item => {
if (item.productBizId == form.value['apiProductLaunchDto'].title) {
submitObj.apiProductLaunchDto.title = item.productName
submitObj.apiProductLaunchDto.productBizId = item.productBizId
}
})
}
// return
saveProductLaunch(submitObj).then(response => {
if (response.code === 200) {
if (route.query.source && route.query.source == 'copy') {
proxy.$modal.msgSuccess('产品复制成功')
emit('handleSuccess', 'baseInfo', response.data)
} else {
proxy.$modal.msgSuccess('产品上架成功')
emit('handleSuccess', 'baseInfo')
}
} else {
proxy.$modal.msgError(response.msg)
}
})
}
watch(
() => props.detailInfo,
newVal => {
if (newVal) {
form.value.apiProductLaunchDto = cloneDeep(newVal.apiProductLaunchDto)
imageInfo.value.count = form.value['apiProductLaunchDto'].mainUrlsList.length
subImageInfo.value.count = form.value['apiProductLaunchDto'].detailUrlsList.length
// 给平台赋值
if (form.value['apiProductLaunchDto'].apiProjectDtoList) {
choosePlatformList.value = cloneDeep(form.value['apiProductLaunchDto'].apiProjectDtoList)
// 更新已选择的ID集合
selectedPlatformIds.value = new Set(choosePlatformList.value.map(item => item.projectBizId))
}
if (newVal.apiSpeciesPriceDtoList.length > 0) {
isSettingSpeciesPrice.value = true
}
if (form.value['apiProductLaunchDto'].isHiddenPrice) {
form.value['apiProductLaunchDto'].isHiddenPrice =
form.value['apiProductLaunchDto'].isHiddenPrice == '1' ? true : false
}
getSelectCategory() // 获取已经选中的分类
/**
* 参数操作:
* 1.先拿到所有参数信息,默认只展示参数列表的第一个,用拿到的所有参数的fieldBizId去请求所有右侧下拉框的options
* 2.第一个参数列表的options也就是左侧的下拉框是参数列表的所有数据,当选了某一个option,下拉框里就没有当前选择的这个option了
* 3.当点击添加参数时,左侧下拉框是除了已经添加的参数外的paramsList外剩余的参数,右侧下拉框是当前参数对应的options
* 回显注意:
* 4.当回显的时候需要依据详情的值对form进行赋值,这一步还未做,等做编辑的时候再做
*/
getProductParamsInfo()
/**
* 产品规格操作
* 1.先拿到所有规格信息,创建新规格默认只展示规格列表的第一个
*/
getProductSpeciesInfo()
showOnce.value = true
searchProduct()
}
}
)
watch(
() => props.activeName,
newVal => {
if (newVal == 'baseInfo') {
// searchProduct()
}
}
)
</script>
<style lang="scss" scoped>
.baseBox {
width: 100%;
box-sizing: border-box;
height: 100%;
/* background-color: rgb(247 247 247) !important; */
/* padding: 0 !important; */
/* height: 500px !important;
overflow: hidden;
overflow-y: scroll; */
}
.cardStyle {
margin-bottom: 20px;
border: none !important;
}
.cardTitle {
font-size: 17px;
font-weight: 600;
}
.chooseClass {
font-size: 14px;
margin: 0px 10px;
}
.editTxt {
font-size: 14px;
color: #576b95;
}
.colBottomGap {
margin-bottom: 30px;
}
.form-content {
width: 100%;
box-sizing: border-box;
height: 73vh;
overflow: hidden;
overflow-y: scroll;
padding: 10px 10px;
}
.form-content :deep(.el-card) {
border: none !important;
}
.formHeader {
margin-bottom: 20px;
font-size: 22px;
font-weight: 600;
}
.nameTip {
font-size: 13px;
margin-top: 10px;
}
.tipCon {
/* width: 450px; */
width: fit-content;
/* 为了更好的兼容性,可以加上带前缀的版本 */
width: -moz-fit-content;
width: -webkit-fit-content;
margin: 5px 0;
padding: 10px 10px;
border-radius: 5px;
background-color: rgb(247 247 247);
font-size: 14px;
display: flex;
align-items: center;
span {
color: rgb(111 111 111);
}
}
.commonHeader {
font-size: 16px;
margin-bottom: 15px;
}
.imgTip {
color: #ccc;
font-size: 14px;
margin-bottom: 10px;
}
.classTip {
color: #ccc;
font-size: 14px;
}
.bottomBtn {
border-top: 1px solid rgb(247 247 247);
width: 100%;
padding: 10px 20px;
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #fff;
}
.paramsItem {
display: flex;
align-items: center;
border: 1px solid rgb(0 0 0 / 10%);
border-radius: 3px;
/* margin-bottom: 20px; */
}
.paramsItem:hover {
border-color: rgba(64, 158, 255, 1);
}
.paramsLeft {
border-right: 1px solid rgb(0 0 0 / 10%);
}
.paramsRight {
border-left: 1px solid rgb(0 0 0 / 10%);
display: flex;
align-items: center;
justify-content: center;
height: 32px;
background-color: #f7f7f7;
}
/* 通用:去除 el-select 和 el-input 的外框 */
.paramsItem :deep(.el-input__wrapper) {
box-shadow: none !important;
}
.paramsItem :deep(.el-select__wrapper) {
box-shadow: none !important;
}
/* 专门针对 el-select:去除获得焦点时的蓝色外框 */
.paramsItem :deep(.el-select .el-input.is-focus .el-input__wrapper) {
box-shadow: none !important;
}
/* 专门针对 el-input:去除获得焦点时的蓝色外框 */
.paramsItem :deep(.el-input .el-input__wrapper.is-focus) {
box-shadow: none !important;
}
.customMiddle {
border-left: 1px solid rgb(0 0 0 / 10%);
}
.errorParams {
background-color: #f7f7f7;
padding: 10px;
border-radius: 5px;
font-size: 14px;
margin: 10px 0px;
display: flex;
align-items: center;
}
.addTxt {
color: #576b95;
font-size: 15px;
display: flex;
align-items: center;
margin-top: 20px;
}
.speciesItem {
background-color: #f7f7f7;
padding: 10px 20px;
margin: 15px 0px;
border-radius: 5px;
}
.speciesTop {
margin-top: 10px;
}
.imgSwitch {
color: rgb(0 0 0 / 55%);
font-size: 14px;
}
.speciesTop {
display: flex;
align-items: center;
justify-content: space-between;
}
.speciesTopLeft {
display: flex;
/* align-items: center; */
}
.customizeInput {
margin-left: 20px;
}
.customizeErrorMsg {
margin-top: 5px;
font-size: 14px;
color: #fa5151;
}
.speciesTopRight {
display: flex;
align-items: center;
justify-content: center;
/* padding: 5px; */
border-radius: 50%;
border: 1px solid rgb(0 0 0 / 10%);
width: 40px;
height: 40px;
}
.speciesLeft {
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
height: 32px;
}
.required-label::before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
.speciesSelect {
margin-bottom: 10px;
display: flex;
}
.settingBox {
display: flex;
}
.settingItem {
width: 220px;
margin-right: 20px;
}
.settingLeft {
color: rgb(0 0 0 / 50%);
font-size: 14px;
text-align: center;
width: 25%;
}
.settingRight {
width: 80%;
}
.errMsg {
color: #f56c6c;
font-size: 12px;
}
.moreBox {
margin-bottom: 20px;
}
.moreTitle {
font-size: 17px;
font-weight: 500;
}
.moreItem {
display: flex;
align-items: center;
justify-content: flex-start;
color: rgb(0 0 0 / 90%);
margin-top: 10px;
font-size: 14px;
}
.moreLeft {
width: 5%;
}
.categoryDialogFooter {
margin-top: 50px;
}
.errorContent {
height: 450px;
overflow-y: scroll;
/* overflow: hidden; */
}
.errorTitle {
font-size: 13px;
color: rgba(0, 0, 0, 50%);
}
.errorItem {
padding: 10px;
background-color: #f7f7f7;
margin: 10px 0px;
border-radius: 5px;
width: 100%;
font-size: 12px;
display: flex;
align-items: center;
}
</style>
<template>
<div class="app-container">
<el-row style="margin-bottom: 20px">
<el-col style="display: flex; justify-content: space-between"
><div>
<el-button type="primary" plain icon="Refresh" @click="updateSpecies">更新规格</el-button>
</div>
</el-col>
</el-row>
<el-table
:data="tableData"
:span-method="objectSpanMethod"
border
style="width: 100%; margin-top: 20px"
row-key="rowKey"
>
<!-- 动态生成所有列 -->
<template v-for="column in tableColumns" :key="column.prop">
<el-table-column :prop="column.prop" :label="column.label" />
</template>
<!-- 固定操作列 -->
<el-table-column
label="操作"
:width="tableData.length > 0 ? 180 : 0"
align="center"
fixed="right"
>
<template #default="scope">
<el-button type="primary" size="small" @click="handleCommissionSetting(scope.row)">
佣金设置
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getSpeciesList"
style="margin-top: 10px"
/>
<!-- 佣金设置 -->
<el-dialog
title="佣金设置"
v-model="showCommisionSetting"
width="1200px"
append-to-body
:close-on-click-modal="false"
>
<el-form :model="settingQueryParams" ref="settingQueryRef" :inline="true" label-width="68px">
<!-- <el-form-item label="项目名称" prop="projectName">
<el-input
v-model="platFormQueryParams.projectName"
placeholder="请输入项目名称"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="searchPlatformTable">搜索</el-button>
<el-button icon="Refresh" @click="paltformReset">重置</el-button>
</el-form-item> -->
</el-form>
<!-- 表格数据 -->
<el-table v-loading="settingLoading" :data="settingList" border ref="settingTableRef">
<el-table-column
label="序号 "
width="55"
align="center"
type="index"
fixed="left"
></el-table-column>
<el-table-column label="费用名称" prop="expenseName" width="150" fixed="left">
<template #header>
<span class="required-label">费用名称</span>
</template>
<template #default="scope">
<el-select v-model="scope.row.expenseName" placeholder="请选择">
<el-option
v-for="item in commission_cost_type"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="佣金年限(起)" prop="startPeriod" width="120">
<template #header>
<span class="required-label">佣金年限(起)</span>
</template>
<template #default="scope">
<el-input
v-model="scope.row.startPeriod"
type="number"
placeholder="请输入"
:min="1"
clearable
@input="handleNumberInput(scope.row, 'startPeriod', 'positive')"
@blur="handlePeriodBlur(scope.row, scope.$index, 'startPeriod')"
/>
</template>
</el-table-column>
<el-table-column label="佣金年限(止)" prop="endPeriod" width="120">
<template #header>
<span class="required-label">佣金年限(止)</span>
</template>
<template #default="scope">
<el-input
v-model="scope.row.endPeriod"
type="number"
:min="1"
placeholder="请输入"
clearable
@input="handleNumberInput(scope.row, 'endPeriod', 'positive')"
@blur="handlePeriodBlur(scope.row, scope.$index, 'endPeriod')"
/>
</template>
</el-table-column>
<el-table-column label="佣金率(%)" prop="commissionRate" width="120">
<template #header>
<span class="required-label">佣金率(%)</span>
</template>
<template #default="scope">
<el-input
v-model="scope.row.commissionRate"
type="number"
:min="0"
placeholder="请输入"
clearable
@input="handleNumberInput(scope.row, 'commissionRate', 'non-negative')"
/>
</template>
</el-table-column>
<el-table-column label="折标系数(%)" prop="discountRatio" width="120">
<template #header>
<span class="required-label">折标系数(%)</span>
</template>
<template #default="scope">
<el-input
v-model="scope.row.discountRatio"
type="number"
:min="0"
placeholder="请输入"
clearable
@input="handleNumberInput(scope.row, 'discountRatio', 'non-negative')"
/>
</template>
</el-table-column>
<el-table-column label="生效日期(起)" prop="effectiveStart" width="150">
<template #header>
<span class="required-label">生效日期(起)</span>
</template>
<template #default="scope">
<el-date-picker
v-model="scope.row.effectiveStart"
style="width: 100%"
type="date"
placeholder="请选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleStartDateChange(scope.row, scope.$index)"
/>
</template>
</el-table-column>
<el-table-column label="生效日期(止)" prop="effectiveEnd" width="150">
<template #header>
<span class="required-label">生效日期(止)</span>
</template>
<template #default="scope">
<el-date-picker
v-model="scope.row.effectiveEnd"
style="width: 100%"
type="date"
placeholder="请选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleEndDateChange(scope.row, scope.$index)"
/>
</template>
</el-table-column>
<el-table-column label="适用范围" prop="scope" width="300">
<template #header>
<span class="required-label">适用范围</span>
</template>
<template #default="scope">
<el-select
v-model="scope.row.scope"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="2"
placeholder="请选择"
clearable
>
<el-option
v-for="item in commission_scope_type"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="是否受汇率影响" prop="isExchangeRate" width="150">
<template #header>
<span class="required-label">是否受汇率影响</span>
</template>
<template #default="scope">
<el-select
v-model="scope.row.isExchangeRate"
style="width: 100%"
placeholder="请选择"
clearable
>
<el-option
v-for="item in sys_no_yes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="结算币种" prop="currency" width="120">
<template #header>
<span class="required-label">结算币种</span>
</template>
<template #default="scope">
<el-select
v-model="scope.row.currency"
style="width: 100%"
placeholder="请选择"
clearable
>
<el-option
v-for="item in bx_currency_type"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="状态" prop="status" width="120">
<template #header>
<span class="required-label">状态</span>
</template>
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="1"
inactive-value="0"
@change="switchChange(scope.row)"
/>
<span style="margin-left: 8px">
{{ scope.row.status === '1' ? '启用' : '禁用' }}
</span>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" width="200">
<template #default="scope">
<el-input v-model="scope.row.remark" type="textarea" placeholder="请输入" />
</template>
</el-table-column>
<el-table-column label="最近一次操作人" prop="updaterName" width="120" />
<el-table-column label="最近一次操作时间" prop="updateTime" width="160">
<template #default="scope">
<span>{{ scope.row.updateTime ? parseTime(scope.row.updateTime) : '--' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<div class="btnCon">
<el-button
v-if="scope.$index == settingList.length - 1"
@click="handleAddCommission(scope.row)"
text
size="small"
type="primary"
>新增</el-button
>
<el-button
text
size="small"
type="danger"
@click="handleDeleteCommission(scope.row, scope.$index)"
>删除</el-button
>
</div>
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeSetting">关闭</el-button>
<el-button type="primary" @click="submitSetting">提 交</el-button>
</div>
</template>
</el-dialog>
<el-dialog title="佣金设置错误提示" v-model="errorTip" width="500px" append-to-body>
<div style="margin-bottom: 10px" v-for="item in settingErrorTip">{{ item }}</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="errorTip = false">去修改</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="SendInfo">
import { cloneDeep } from 'lodash-es'
import {
exportSpecies,
sendSpecies,
sendCommissionRatio,
saveBatchSendCommission,
deleteCommission,
changeSendStatus
} from '@/api/product/index'
import { ref, watch } from 'vue'
const props = defineProps({
// 类型,是新增还是编辑,
activeName: {
type: String,
default: ''
}
})
const route = useRoute()
const { proxy } = getCurrentInstance()
const loading = ref(true)
const settingLoading = ref(true)
const total = ref(0) //规格表格的总条数
const settingTotal = ref(0) //规格表格的总条数
const dateRange = ref([])
const showCommisionSetting = ref(false) //佣金设置弹窗开关
const errorTip = ref(false) //佣金设置弹窗开关
const settingList = ref([]) //佣金设置表格数据
const currentSettingRow = ref([]) //当前选中的佣金设置行
const settingErrorTip = ref([]) //提交佣金错误提示信息
const data = reactive({
queryParams: {
pageNo: 1,
pageSize: 10,
productLaunchBizId: route.query.productLaunchBizId
},
settingQueryParams: {
pageNo: 1,
pageSize: 9999
}
})
const { queryParams, settingQueryParams } = toRefs(data)
const { bx_currency_type, commission_scope_type, commission_cost_type, sys_no_yes, sys_status } =
proxy.useDict(
'bx_currency_type',
'commission_scope_type',
'commission_cost_type',
'sys_no_yes',
'sys_status'
)
const switchChange = row => {
if (!row.announcementCommissionRatioBizId) {
return
}
try {
changeSendStatus({
announcementCommissionRatioBizId: row.announcementCommissionRatioBizId,
status: Number(row.status)
}).then(response => {
if (response.code === 200) {
proxy.$modal.msgSuccess('状态修改成功')
}
})
} catch (error) {
row.status = row.status === '1' ? '0' : '1'
}
}
const handleDeleteCommission = (row, index) => {
if (settingList.value.length === 1) {
proxy.$modal.msgError('至少保留一条佣金设置数据')
return
}
proxy.$modal
.confirm(`是否确认删除第${index + 1}行数据?`)
.then(function () {
if (row.announcementCommissionRatioBizId) {
deleteCommission(row.announcementCommissionRatioBizId).then(response => {
if (response.code === 200) {
settingList.value.splice(index, 1)
} else {
proxy.$modal.msgError(response.msg)
}
})
} else {
settingList.value.splice(index, 1)
}
proxy.$modal.msgSuccess('删除成功')
})
.catch(() => {})
}
const handleNumberInput = (row, field, type = 'positive') => {
const value = row[field]
if (value === null || value === undefined || value === '') return
let numValue = Number(value)
if (isNaN(numValue)) {
row[field] = null
return
}
if (type === 'positive') {
// 正整数:移除负号和小数部分
if (numValue <= 0) {
row[field] = null
return
}
// 取整数部分
row[field] = Math.floor(Math.abs(numValue))
} else if (type === 'non-negative') {
// 非负数:允许0及以上,不允许负数
if (numValue < 0) {
row[field] = 0
}
}
}
// 处理开始日期变化
const handleStartDateChange = (row, index) => {
if (row.effectiveStart && row.effectiveEnd) {
const start = new Date(row.effectiveStart)
const end = new Date(row.effectiveEnd)
if (start > end) {
proxy.$modal.msgError(
`第${index + 1}行:生效日期(起)不能晚于生效日期(止),请重新选择生效日期(起)`
)
// 清空结束日期,让用户重新选择
setTimeout(() => {
row.effectiveStart = ''
}, 0)
}
}
}
// 处理结束日期变化
const handleEndDateChange = (row, index) => {
console.log('时间', row)
if (row.effectiveStart && row.effectiveEnd) {
const start = new Date(row.effectiveStart)
const end = new Date(row.effectiveEnd)
if (end < start) {
proxy.$modal.msgError(
`第${index + 1}行:生效日期(止)不能早于生效日期(起),请重新选择生效日期(止)`
)
// 清空开始日期,让用户重新选择
setTimeout(() => {
row.effectiveEnd = ''
}, 0)
}
}
}
const handlePeriodBlur = (row, index, key) => {
if (row.startPeriod && row.endPeriod && row.startPeriod > row.endPeriod) {
proxy.$modal.msgError(`第${index + 1}行:佣金开始年限不能大于佣金结束年限,请重新填写`)
row[key] = ''
row.unadd = true
} else {
row.unadd = false
}
console.log('年限', row)
}
const handleAddCommission = row => {
let newRow = cloneDeep(row)
let obj = {}
// 复制当前条的数据
for (const key in newRow) {
if (key !== 'startPeriod' && key !== 'endPeriod' && key !== 'commissionRate') {
obj[key] = newRow[key]
} else {
obj[key] = ''
}
}
if (!row.unadd) {
settingList.value.push(obj)
}
}
const closeSetting = () => {
showCommisionSetting.value = false
settingQueryParams.value = {
pageNo: 1,
pageSize: 9999
}
}
// 批量提交佣金设置
const submitSetting = async () => {
// 校验表格数据
const validationResult = validateTableData()
if (!validationResult.valid) {
proxy.$modal.msgError(validationResult.message)
return
}
let saveTable = cloneDeep(settingList.value)
saveTable.forEach(row => {
if (row.scope.length > 0) {
row.scope = row.scope.join(';')
}
})
console.log('saveTable', saveTable)
// return
try {
saveBatchSendCommission({
announcementSpeciesBizId: currentSettingRow.value.announcementSpeciesBizId,
ratioBatchSaveDtoList: saveTable
}).then(response => {
console.log('response', response)
if (response.code === 200) {
showCommisionSetting.value = false
proxy.$modal.msgSuccess('提交成功')
} else if (response.code === 1004) {
if (response.msg) {
settingErrorTip.value = response.msg.split('\n').filter(item => item !== '')
}
errorTip.value = true
}
})
} catch (error) {
console.log('错误了', error)
}
}
// 表格数据校验函数
const validateTableData = () => {
const errors = []
const requiredFields = [
{ key: 'expenseName', label: '费用名称' },
{ key: 'startPeriod', label: '佣金年限(起)' },
{ key: 'endPeriod', label: '佣金年限(止)' },
{ key: 'commissionRate', label: '佣金率(%)' },
{ key: 'discountRatio', label: '折标系数(%)' },
{ key: 'effectiveStart', label: '生效日期(起)' },
{ key: 'effectiveEnd', label: '生效日期(止)' },
{ key: 'scope', label: '适用范围' },
{ key: 'isExchangeRate', label: '是否受汇率影响' },
{ key: 'currency', label: '结算币种' },
{ key: 'status', label: '状态' }
]
settingList.value.forEach((row, index) => {
requiredFields.forEach(field => {
const value = row[field.key]
let isValid = true
if (Array.isArray(value)) {
isValid = value.length > 0
} else {
isValid = value !== null && value !== undefined && value !== ''
}
if (!isValid) {
errors.push({
rowIndex: index,
field: field.key,
label: field.label,
rowNumber: index + 1
})
}
})
})
if (errors.length === 0) {
return { valid: true }
}
// 生成友好的错误提示信息
const errorMessages = errors
.slice(0, 3)
.map(error => `第${error.rowNumber}行【${error.label}】未填写`)
let message = errorMessages.join(';')
if (errors.length > 3) {
message += `等,共${errors.length}处必填项未填写`
}
return {
valid: false,
errors,
message
}
}
/** 获得佣金设置数据 */
function getSettingList() {
settingLoading.value = true
let obj = {
expenseName: '',
startPeriod: '',
endPeriod: '',
commissionRate: '',
discountRatio: '',
effectiveStart: '',
effectiveEnd: '',
scope: '',
isExchangeRate: '',
currency: '',
status: '',
remark: ''
}
settingList.value = []
settingQueryParams.value.announcementSpeciesBizId =
currentSettingRow.value.announcementSpeciesBizId
sendCommissionRatio(settingQueryParams.value).then(response => {
if (response.code === 200) {
if (response.data.records.length > 0) {
settingList.value = response.data.records
settingList.value.forEach(row => {
if (row.scope) {
row.scope = row.scope.split(';')
}
row.status = row.status === 1 ? '1' : row.status === 0 ? '0' : ''
})
} else {
settingList.value.push(obj)
}
settingTotal.value = response.data.total
settingLoading.value = false
showCommisionSetting.value = true
} else {
settingLoading.value = false
proxy.$modal.msgError(response.msg)
}
})
}
// 原始数据
const originalData = ref([])
// 表格列配置
const tableColumns = ref([])
// 表格数据
const tableData = ref([])
// 合并行配置,存储每列需要合并的行数
const spanConfig = ref({})
// 从数据中提取所有可能的列
const extractColumnsFromData = data => {
const columnSet = new Map()
data.forEach(item => {
try {
if (item.apiSpeciesSettingDtoList && item.apiSpeciesSettingDtoList.length > 0) {
item.apiSpeciesSettingDtoList.forEach(species => {
if (!columnSet.has(species.typeCode)) {
columnSet.set(species.typeCode, {
prop: species.typeCode,
label: species.typeName,
width: species.typeName.length * 15 + 30,
align: 'center',
sortable: false
})
}
})
}
} catch (e) {
console.log('计算列失败', e)
}
})
// 将 Map 转换为数组并按 typeCode 排序(可选)
return Array.from(columnSet.values())
}
// 处理数据,生成表格需要的格式
const processTableData = () => {
if (!originalData.value || originalData.value.length === 0) {
tableData.value = []
spanConfig.value = {}
return
}
console.log('originalData.value', originalData.value)
// 1. 提取所有列
const columns = extractColumnsFromData(originalData.value)
tableColumns.value = columns
// 2. 转换数据格式
const processedData = []
originalData.value.forEach((item, index) => {
const rowData = {
rowKey: `${item.id}_${index}`, // 生成唯一 rowKey
id: item.id, // 保留原始ID
rawData: item // 保留原始数据
}
// 填充所有列的值
columns.forEach(column => {
try {
if (item.apiSpeciesSettingDtoList && item.apiSpeciesSettingDtoList.length > 0) {
const matchedSpecies = item.apiSpeciesSettingDtoList.find(s => s.typeCode === column.prop)
rowData[column.prop] = matchedSpecies ? matchedSpecies.value : ''
} else {
rowData[column.prop] = ''
}
} catch (e) {
console.log('填充所有列的值失败', e)
}
})
processedData.push(rowData)
})
// 3. 计算合并配置
calculateSpanConfig(processedData, columns)
tableData.value = processedData
}
// 计算合并配置
const calculateSpanConfig = (data, columns) => {
const config = {}
// 为每一列计算合并配置
columns.forEach((column, columnIndex) => {
config[column.prop] = []
let pos = 0
while (pos < data.length) {
const currentValue = data[pos][column.prop]
let rowspan = 1
// 向后查找相同值的行
for (let i = pos + 1; i < data.length; i++) {
// 只有当该列之前的所有列的值都相同时才合并
let shouldMerge = true
for (let j = 0; j < columnIndex; j++) {
const prevColumn = columns[j]
if (data[pos][prevColumn.prop] !== data[i][prevColumn.prop]) {
shouldMerge = false
break
}
}
if (shouldMerge && data[i][column.prop] === currentValue) {
rowspan++
} else {
break
}
}
// 记录合并配置
config[column.prop][pos] = rowspan
// 跳过已经计算的行
pos += rowspan
}
// 填充剩余的配置为0(不合并)
for (let i = 0; i < data.length; i++) {
if (!config[column.prop][i]) {
config[column.prop][i] = 0
}
}
})
spanConfig.value = config
}
// 合并单元格的方法
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
// 最后一列是操作列,不合并
if (columnIndex === tableColumns.value.length) {
return {
rowspan: 1,
colspan: 1
}
}
const columnProp = tableColumns.value[columnIndex].prop
const rowspan = spanConfig.value[columnProp]?.[rowIndex] || 0
if (rowspan > 0) {
return {
rowspan: rowspan,
colspan: 1
}
} else {
return {
rowspan: 0,
colspan: 0
}
}
}
// 佣金设置操作
const handleCommissionSetting = row => {
currentSettingRow.value = row.rawData
console.log('佣金设置', currentSettingRow.value)
getSettingList()
}
/** 获得规格数据 */
function getSpeciesList() {
loading.value = true
sendSpecies(queryParams.value).then(response => {
if (response.code === 200) {
originalData.value = response.data.records
total.value = response.data.total
loading.value = false
if (originalData.value.length == 0) {
proxy.$modal.msgError('该商品暂无规格数据,请先更新规格数据')
} else {
let codeArr = []
// 自定义没有typeCode影响表格得展示
originalData.value[0].apiSpeciesSettingDtoList.forEach((item, index) => {
if (item.isCustomize == '1') {
item.typeCode = `custom${Date.now() + Math.floor(Math.random() * 1000)}`
codeArr.push({ index: index, code: item.typeCode })
}
})
originalData.value.forEach((item, oIndex) => {
item.apiSpeciesSettingDtoList.forEach((species, sIndex) => {
if (codeArr.length > 0) {
codeArr.forEach(codeItem => {
if (codeItem.index == sIndex) {
species.typeCode = codeItem.code
}
})
}
})
})
}
// 计算表格
processTableData()
} else {
proxy.$modal.msgError(response.msg)
}
})
}
/** 更新规格数据 */
function updateSpecies() {
loading.value = true
exportSpecies({ productLaunchBizId: route.query.productLaunchBizId }).then(response => {
if (response.code === 200) {
proxy.$modal.msgSuccess('发佣管理-规格更新成功')
getSpeciesList()
} else {
proxy.$modal.msgError(response.msg)
}
})
}
//========多选下拉框悬停效果结束=========
watch(
() => props.activeName,
newVal => {
if (newVal == 'sendCommission') {
getSpeciesList()
}
}
)
</script>
<style lang="scss" scoped>
.required-label::before {
content: '*';
color: #f56c6c;
margin-right: 4px;
}
.btnCon {
display: flex;
align-items: center;
justify-content: center;
}
/* 错误行样式 */
.error-row {
background-color: #fff2f0 !important;
animation: highlight 0.6s ease-in-out;
}
/* 错误字段高亮 */
.error-field :deep(.el-input__wrapper) {
border-color: #f56c6c !important;
box-shadow: 0 0 0 1px #f56c6c !important;
}
.error-field :deep(.el-input__inner) {
color: #f56c6c;
}
.error-field :deep(.el-select__wrapper) {
border-color: #f56c6c !important;
box-shadow: 0 0 0 1px #f56c6c !important;
}
/* 闪烁动画吸引用户注意力 */
@keyframes highlight {
0%,
50% {
background-color: #fff2f0;
}
25%,
75% {
background-color: #ffeaea;
}
100% {
background-color: #fff2f0;
}
}
/* 错误提示消息样式 */
.validation-message-box {
:deep(.el-message-box__content) {
max-height: 400px;
overflow-y: auto;
}
}
</style>
<template>
<div class="app-container">
<el-tabs v-model="activeName" class="demo-tabs" :before-leave="beforeTabLeave">
<el-tab-pane v-for="tab in tabsList" :key="tab.name" :label="tab.label" :name="tab.name">
<el-row>
<el-form :model="queryParams" :inline="true">
<el-form-item label="标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入标题"
clearable
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格数据 -->
<el-table v-loading="loading" :data="tableList">
<el-table-column label="商品图片" align="center" prop="mainUrls" width="150">
<template #default="scope">
<el-image
v-if="scope.row.mainUrls"
:src="scope.row.mainUrls"
class="logo-image"
fit="cover"
>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="标题" prop="title" :show-overflow-tooltip="true" />
<el-table-column
label="短标题"
prop="shortTitle"
:show-overflow-tooltip="true"
width="150"
/>
<el-table-column label="状态" prop="status" align="left">
<template #default="scope">
<dict-tag :options="product_launch_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
class-name="small-padding fixed-width"
width="200"
fixed="right"
>
<template #default="scope">
<div class="btnCon">
<el-button text size="small" type="primary" @click="handleEdit(scope.row)"
>编辑</el-button
>
<!-- <el-button text size="small" type="danger">删除</el-button> -->
<el-dropdown placement="left" style="margin-left: 10px">
<el-button type="primary" link size="small">
更多 <el-icon><ArrowDown /></el-icon
></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-if="scope.row.status == 'DSH' || scope.row.status == 'SB'"
@click="changeStatus(scope.row, 'SH')"
>审核</el-dropdown-item
>
<el-dropdown-item
v-if="scope.row.status == 'XJ'"
@click="changeStatus(scope.row, 'SJ')"
>上架</el-dropdown-item
>
<el-dropdown-item
v-if="scope.row.status == 'ZS' || scope.row.status == 'YS'"
@click="changeStatus(scope.row, 'XJ')"
>下架</el-dropdown-item
>
<el-dropdown-item @click="handleEdit(scope.row, 'copy')"
>复制</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
<el-col style="display: flex; justify-content: flex-end">
<pagination
v-show="total >= 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getTableList"
/>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
<el-dialog title="审核" v-model="showReview" width="500px" append-to-body>
<el-form :model="reviewForm" label-width="68px">
<el-form-item label="审核状态" prop="approvalStatus">
<el-radio-group v-model="reviewForm.approvalStatus">
<el-radio value="TG" size="large">通过</el-radio>
<el-radio value="WTG" size="large">不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="reviewForm.approvalStatus == 'WTG'" label="备注">
<el-input
v-model="reviewForm.approvalRemark"
placeholder="请输入备注"
clearable
type="textarea"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="confirmReview">提交审核</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="InsuranceProduct">
import { goodsList, changeProductStatus, productApproval } from '@/api/product/index'
import { ref, watch } from 'vue'
const router = useRouter()
const route = useRoute()
const { proxy } = getCurrentInstance()
const { product_launch_status } = proxy.useDict('product_launch_status')
const activeName = ref('all')
const total = ref(0)
const loading = ref(false)
const tableList = ref([])
const showReview = ref(false)
const currentRow = ref({})
const data = reactive({
queryParams: {},
allQueryParams: {
pageNo: 1,
pageSize: 10,
selectType: null
},
saleQueryParams: {
pageNo: 1,
pageSize: 10,
selectType: 1
},
delistQueryParams: {
pageNo: 1,
pageSize: 10,
selectType: 2
},
reviewForm: {},
rules: {
approvalStatus: [{ required: true, message: '审核状态不能为空', trigger: 'blur' }]
}
})
const { queryParams, allQueryParams, saleQueryParams, delistQueryParams, reviewForm } = toRefs(data)
const tabsList = ref([
{
label: '全部',
name: 'all',
key: 'all',
id: 1
},
{
label: '销售中',
name: 'sale',
id: 2,
key: 'sale'
},
{
label: '已下架',
name: 'delist',
id: 3,
key: 'delist'
}
])
// Tab切换前的验证
const beforeTabLeave = (activeTabName, oldTabName) => {
return true
}
const handleEdit = (row, type) => {
let obj = {
productBizId: row.productBizId,
productLaunchBizId: row.productLaunchBizId
}
if (type && type == 'copy') {
obj.source = 'copy'
}
router.push({
path: '/merchandise/putOnSale',
query: {
...obj
}
})
}
// 提交审核
const confirmReview = () => {
if (!reviewForm.value.approvalStatus) {
proxy.$modal.msgError('请选择审核状态')
return
}
productApproval({
productLaunchBizId: currentRow.value.productLaunchBizId,
...reviewForm.value
}).then(response => {
if (response.code === 200) {
proxy.$modal.msgSuccess(`已提交审核`)
getTableList()
showReview.value = false
reviewForm.value = {}
} else {
proxy.$modal.msgError(response.msg)
}
})
}
// 上下架
const changeStatus = (row, type) => {
console.log('type', type)
currentRow.value = { ...row }
if (type == 'SH') {
showReview.value = true
} else if (type == 'SJ' || type == 'XJ') {
proxy.$modal
.confirm(`是否确认${type == 'SJ' ? '上架' : '下架'}${row.title}?`)
.then(function () {
changeProductStatus({
productLaunchBizId: row.productLaunchBizId,
status: type
}).then(response => {
if (response.code === 200) {
proxy.$modal.msgSuccess(`${type == 'SJ' ? '上架' : '下架'}成功`)
getTableList()
} else {
proxy.$modal.msgError(response.msg)
}
})
})
.catch(() => {})
}
}
const handleQuery = () => {
let obj = {
pageNo: 1,
pageSize: 10
}
allQueryParams.value = { ...allQueryParams.value, ...obj }
saleQueryParams.value = { ...saleQueryParams.value, ...obj }
delistQueryParams.value = { ...delistQueryParams.value, ...obj }
if (activeName.value == 'all') {
queryParams.value = allQueryParams.value
} else if (activeName.value == 'sale') {
queryParams.value = saleQueryParams.value
} else if (activeName.value == 'delist') {
queryParams.value = delistQueryParams.value
}
getTableList()
}
const resetQuery = () => {
let obj = {
pageNo: 1,
pageSize: 10,
title: null
}
allQueryParams.value = { ...allQueryParams.value, ...obj }
saleQueryParams.value = { ...saleQueryParams.value, ...obj }
delistQueryParams.value = { ...delistQueryParams.value, ...obj }
if (activeName.value == 'all') {
queryParams.value = allQueryParams.value
} else if (activeName.value == 'sale') {
queryParams.value = saleQueryParams.value
} else if (activeName.value == 'delist') {
queryParams.value = delistQueryParams.value
}
getTableList()
}
const getTableList = () => {
loading.value = true
goodsList(queryParams.value).then(response => {
if (response.code === 200) {
tableList.value = response.data.records
total.value = response.data.total
loading.value = false
tableList.value.forEach(item => {
if (item.mainUrls) {
item.mainUrls = item.mainUrls.split(';')[0]
}
})
} else {
proxy.$modal.msgError(response.msg)
}
})
}
watch(
() => activeName.value,
newVal => {
if (newVal == 'all') {
queryParams.value = allQueryParams.value
} else if (newVal == 'sale') {
queryParams.value = saleQueryParams.value
} else if (newVal == 'delist') {
queryParams.value = delistQueryParams.value
}
getTableList()
}
)
queryParams.value = allQueryParams.value
getTableList()
</script>
<style lang="scss" scoped>
.app-container {
width: 100%;
box-sizing: border-box;
}
.logo-image {
width: 70px;
height: 70px;
border-radius: 4px;
object-fit: cover;
}
.btnCon {
display: flex;
align-items: center;
justify-content: center;
}
</style>
<template>
<div class="app-container">
<el-tabs v-model="activeName" class="demo-tabs" :before-leave="beforeTabLeave">
<el-tab-pane v-for="tab in tabsList" :key="tab.name" :label="tab.label" :name="tab.name">
<ProductBaseInfo
:activeName="activeName"
v-if="tab.name === 'baseInfo'"
:detailInfo="productBaseInfo"
@handleSuccess="handleSuccess"
/>
<div v-if="tab.name === 'comeCommission'">
<ComeInfo :activeName="activeName"></ComeInfo>
</div>
<div v-if="tab.name === 'sendCommission'">
<SendInfo :activeName="activeName"></SendInfo>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup name="InsuranceProduct">
import { getProductDetail } from '@/api/product/index'
import { ref } from 'vue'
import ProductBaseInfo from './components/productBaseInfo.vue'
import SendInfo from './components/sendInfo.vue'
import ComeInfo from './components/comeInfo.vue'
const route = useRoute()
const { proxy } = getCurrentInstance()
const activeName = ref('baseInfo')
const productBaseInfo = ref({}) //产品基本信息
const tabsList = ref([
{
label: '基础信息',
name: 'baseInfo',
key: 'baseInfo',
id: 1
},
{
label: '来佣管理',
name: 'comeCommission',
id: 2,
key: 'comeCommission'
},
{
label: '发佣管理',
name: 'sendCommission',
id: 3,
key: 'sendCommission'
}
])
const handleSuccess = (source, id) => {
if (source === 'baseInfo') {
getProductInfo('comeCommission', id)
}
}
// Tab切换前的验证
const beforeTabLeave = (activeTabName, oldTabName) => {
if (activeTabName !== 'baseInfo' && productBaseInfo.value.apiSpeciesPriceDtoList.length == 0) {
proxy.$modal.msgError('请先提交产品相关信息,再进行下一步操作!')
return false
}
return true
}
/** 获取产品详情 */
function getProductInfo(tabName, id) {
let productLaunchBizId = ''
if (route.query.source && route.query.source == 'copy') {
productLaunchBizId = id
} else {
productLaunchBizId = route.query.productLaunchBizId
}
getProductDetail(productLaunchBizId).then(response => {
if (response.code === 200) {
productBaseInfo.value = response.data
activeName.value = tabName
} else {
proxy.$modal.msgError(response.msg)
}
})
}
if (route.query.source && route.query.source == 'copy') {
getProductInfo('baseInfo', route.query.productLaunchBizId)
} else {
getProductInfo('baseInfo')
}
</script>
<style lang="scss" scoped>
.app-container {
width: 100%;
height: 89.8vh !important;
box-sizing: border-box;
/* background-color: rgb(247 247 247) !important; */
/* padding: 0 !important; */
/* height: 500px !important;
overflow: hidden;
overflow-y: scroll; */
}
</style>
<template>
<div class="pdf-viewer-container">
<!-- 控制面板 -->
<div class="control-panel">
<button @click="loadPDF" :disabled="loading">加载PDF</button>
<button @click="downloadAndFixPDF" :disabled="loading">下载并修复PDF</button>
<button @click="testSmallChunk" :disabled="loading">测试小文件下载</button>
<button @click="resetViewer" :disabled="loading">重置</button>
<span class="page-info" v-if="totalPages > 0">{{ currentPage }} 页 / 共 {{ totalPages }}</span>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="progress-info">
<div v-if="downloadProgress < 100">
下载进度: {{ downloadProgress }}%
<div class="progress-bar">
<div class="progress-fill" :style="{ width: downloadProgress + '%' }"></div>
</div>
<div>已下载: {{ loadedProgressText }}</div>
</div>
<div v-else>
PDF处理中...
</div>
</div>
</div>
<!-- 错误信息 -->
<div v-if="error" class="error-message">
{{ error }}
</div>
<!-- PDF 渲染区域 -->
<div ref="pdfContainer" class="pdf-container" :class="{ hidden: loading }">
<iframe
v-if="pdfUrl"
:src="pdfUrl"
width="100%"
height="600px"
style="border: none;"
@load="onIframeLoad"
@error="onIframeError"
></iframe>
<div v-else-if="!loading" class="no-pdf-message">
暂无PDF显示
</div>
</div>
<!-- 调试信息 -->
<div v-if="debugInfo" class="debug-info">
{{ debugInfo }}
</div>
<!-- 下载链接 -->
<div v-if="pdfUrl" style="margin-top: 10px;">
<a :href="pdfUrl" download="document.pdf" style="color: green; font-weight: bold;">
↓ 下载PDF文件
</a>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'PDFViewer',
data() {
return {
fileKey: 'pdf/2025/10/11/company-intro.pdf',
currentPage: 1,
totalPages: 0,
loading: false,
error: '',
debugInfo: '',
totalSize: 0,
loadedSize: 0,
pdfUrl: '',
downloadProgress: 0,
// 配置项
chunkSize: 2 * 1024 * 1024, // 2MB 每块
maxConcurrentDownloads: 3, // 最大并发下载数
};
},
computed: {
loadedProgressText() {
if (this.totalSize === 0) return '';
const loadedMB = (this.loadedSize / 1024 / 1024).toFixed(1);
const totalMB = (this.totalSize / 1024 / 1024).toFixed(1);
return `${loadedMB}MB / ${totalMB}MB`;
}
},
methods: {
async loadPDF() {
await this.downloadAndRenderPDF();
},
async downloadAndFixPDF() {
await this.downloadAndRenderPDF(true);
},
async downloadAndRenderPDF(attemptFix = false) {
this.loading = true;
this.error = '';
this.downloadProgress = 0;
this.pdfUrl = '';
try {
this.debugInfo = '开始加载PDF...';
// 获取文件大小
const fileSize = await this.getFileSize();
this.totalSize = fileSize;
this.debugInfo = `文件大小: ${(fileSize / 1024 / 1024).toFixed(1)}MB`;
// 分块下载完整文件
const fullPdfData = await this.downloadFileByChunks(fileSize);
// 检查并修复PDF
let processedData = fullPdfData;
if (attemptFix) {
processedData = await this.attemptFixPDF(fullPdfData);
}
// 使用 iframe 渲染
await this.renderWithIframe(processedData);
this.debugInfo = `PDF 加载完成,大小: ${(processedData.byteLength / 1024 / 1024).toFixed(1)}MB`;
} catch (error) {
this.error = `PDF 加载失败: ${error.message}`;
console.error('PDF 加载失败:', error);
} finally {
this.loading = false;
}
},
// 尝试修复PDF文件
async attemptFixPDF(pdfData) {
this.debugInfo = '尝试修复PDF文件...';
const dataView = new DataView(pdfData);
const dataArray = new Uint8Array(pdfData);
// 检查文件头
const header = String.fromCharCode(...dataArray.slice(0, 8));
console.log('PDF文件头:', header);
if (header.substring(0, 4) !== '%PDF') {
throw new Error('无效的PDF文件头');
}
// 查找文件尾的xref表和trailer
const trailerInfo = this.findTrailerAndXref(dataArray);
console.log('Trailer信息:', trailerInfo);
if (!trailerInfo.foundTrailer) {
console.warn('未找到完整的trailer,尝试手动添加EOF');
// 手动添加EOF标记
return this.addManualEOF(dataArray);
}
return pdfData;
},
// 查找trailer和xref表
findTrailerAndXref(dataArray) {
const decoder = new TextDecoder('ascii');
const dataString = decoder.decode(dataArray);
// 从文件末尾开始查找
const chunkSize = 1024;
let foundTrailer = false;
let foundStartXref = false;
let xrefOffset = -1;
// 检查最后几个chunk
for (let i = 0; i < 5; i++) {
const start = Math.max(0, dataArray.length - chunkSize * (i + 1));
const end = dataArray.length - chunkSize * i;
const chunk = dataArray.slice(start, end);
const chunkString = decoder.decode(chunk);
if (chunkString.includes('trailer')) {
foundTrailer = true;
}
if (chunkString.includes('startxref')) {
foundStartXref = true;
// 提取xref偏移量
const startxrefMatch = chunkString.match(/startxref\s+(\d+)/);
if (startxrefMatch) {
xrefOffset = parseInt(startxrefMatch[1]);
}
}
if (chunkString.includes('%%EOF')) {
console.log('找到EOF标记');
}
}
return {
foundTrailer,
foundStartXref,
xrefOffset
};
},
// 手动添加EOF标记
addManualEOF(dataArray) {
this.debugInfo = '手动添加PDF结束标记...';
// 创建新的ArrayBuffer,额外空间用于添加EOF
const newBuffer = new ArrayBuffer(dataArray.length + 50);
const newArray = new Uint8Array(newBuffer);
// 复制原始数据
newArray.set(dataArray, 0);
// 添加基本的PDF结束结构
const eofString = '\n\n%%EOF\n';
const encoder = new TextEncoder();
const eofBytes = encoder.encode(eofString);
newArray.set(eofBytes, dataArray.length);
console.log('已添加手动EOF标记');
return newArray.buffer;
},
// 测试小文件下载
async testSmallChunk() {
this.loading = true;
this.error = '';
try {
this.debugInfo = '测试小文件下载...';
// 只下载前5MB
const testSize = 5 * 1024 * 1024;
const testData = await this.downloadChunk(0, testSize - 1);
// 检查下载的数据
await this.analyzePDFChunk(testData);
this.debugInfo = '小文件测试完成';
} catch (error) {
this.error = `测试失败: ${error.message}`;
} finally {
this.loading = false;
}
},
// 分析PDF数据块
async analyzePDFChunk(pdfData) {
const dataArray = new Uint8Array(pdfData);
const decoder = new TextDecoder('ascii');
// 检查文件头
const header = String.fromCharCode(...dataArray.slice(0, 8));
console.log('测试文件头:', header);
// 检查文件内容
const sampleSize = Math.min(1000, dataArray.length);
const sampleStart = decoder.decode(dataArray.slice(0, sampleSize));
const sampleEnd = decoder.decode(dataArray.slice(-sampleSize));
console.log('文件开始样本:', sampleStart.substring(0, 200));
console.log('文件结束样本:', sampleEnd.substring(Math.max(0, sampleEnd.length - 200)));
// 检查关键标记
const hasObj = sampleStart.includes('obj');
const hasEndObj = sampleStart.includes('endobj');
const hasXref = sampleEnd.includes('xref');
const hasTrailer = sampleEnd.includes('trailer');
const hasEOF = sampleEnd.includes('%%EOF');
console.log('PDF结构检查:', {
hasObj,
hasEndObj,
hasXref,
hasTrailer,
hasEOF
});
this.debugInfo = `PDF结构: 对象${hasObj ? '✓' : '✗'} XREF${hasXref ? '✓' : '✗'} Trailer${hasTrailer ? '✓' : '✗'} EOF${hasEOF ? '✓' : '✗'}`;
},
// 分块下载文件
async downloadFileByChunks(totalSize) {
const chunks = [];
const chunkCount = Math.ceil(totalSize / this.chunkSize);
console.log(`开始分块下载,总大小: ${(totalSize / 1024 / 1024).toFixed(1)}MB, 块数: ${chunkCount}`);
// 创建所有下载任务
const chunkPromises = [];
for (let i = 0; i < chunkCount; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize - 1, totalSize - 1);
chunkPromises.push(() => this.downloadChunkWithProgress(start, end, i, chunkCount));
}
// 并发下载(限制并发数)
for (let i = 0; i < chunkPromises.length; i += this.maxConcurrentDownloads) {
const batch = chunkPromises.slice(i, i + this.maxConcurrentDownloads);
const batchResults = await Promise.all(batch.map(fn => fn()));
chunks.push(...batchResults);
console.log(`已完成 ${Math.min(i + this.maxConcurrentDownloads, chunkCount)}/${chunkCount} 块下载`);
}
// 合并所有块
return this.mergeChunks(chunks);
},
// 下载单个块(带进度)
async downloadChunkWithProgress(start, end, chunkIndex, totalChunks) {
try {
const response = await axios.get('http://10.0.10.215:9106/oss/api/api/file/chunk', {
params: {
fileKey: this.fileKey,
start: start,
end: end
},
responseType: 'arraybuffer',
onDownloadProgress: (progressEvent) => {
// 计算整体下载进度
const chunkProgress = progressEvent.loaded ? (progressEvent.loaded / (end - start + 1)) * 100 : 0;
const overallProgress = (chunkIndex / totalChunks) * 100 + (chunkProgress / totalChunks);
this.downloadProgress = Math.min(100, Math.round(overallProgress));
}
});
this.loadedSize = Math.max(this.loadedSize, end + 1);
return response.data;
} catch (error) {
console.error(`下载块 ${start}-${end} 失败:`, error);
throw error;
}
},
// 下载单个块(无进度)
async downloadChunk(start, end) {
try {
const response = await axios.get('http://10.0.10.215:9106/oss/api/api/file/chunk', {
params: {
fileKey: this.fileKey,
start: start,
end: end
},
responseType: 'arraybuffer'
});
return response.data;
} catch (error) {
console.error(`下载块 ${start}-${end} 失败:`, error);
throw error;
}
},
// 合并所有块
mergeChunks(chunks) {
const totalLength = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
chunks.forEach(chunk => {
const chunkArray = new Uint8Array(chunk);
result.set(chunkArray, offset);
offset += chunkArray.length;
});
console.log('合并完成,总大小:', totalLength);
return result;
},
// 使用 iframe 渲染
async renderWithIframe(pdfData) {
try {
this.debugInfo = '使用 iframe 渲染 PDF...';
// 创建 Blob 并生成 URL
const blob = new Blob([pdfData], { type: 'application/pdf' });
this.pdfUrl = URL.createObjectURL(blob);
this.debugInfo = 'iframe PDF 渲染完成';
this.currentPage = 1;
this.totalPages = 1; // iframe 模式下我们不知道总页数
} catch (error) {
console.error('iframe 渲染失败:', error);
throw error;
}
},
// iframe 加载成功
onIframeLoad() {
console.log('iframe PDF 加载成功');
this.debugInfo += ' | iframe 加载成功';
},
// iframe 加载失败
onIframeError() {
console.error('iframe PDF 加载失败');
this.error = 'iframe 无法加载 PDF 文档';
},
// 获取文件大小
async getFileSize() {
try {
const response = await axios.get('http://10.0.10.215:9106/oss/api/api/file/chunk', {
params: {
fileKey: this.fileKey,
start: 0,
end: 1024
},
responseType: 'arraybuffer'
});
// 尝试从响应头获取文件大小
const contentRange = response.headers['content-range'];
if (contentRange) {
const match = contentRange.match(/\/(\d+)$/);
if (match) {
return parseInt(match[1]);
}
}
// 使用默认值
return 85 * 1024 * 1024;
} catch (error) {
console.error('获取文件大小失败,使用默认值');
return 85 * 1024 * 1024;
}
},
// 重置查看器
resetViewer() {
if (this.pdfUrl) {
URL.revokeObjectURL(this.pdfUrl);
}
this.currentPage = 1;
this.totalPages = 0;
this.error = '';
this.debugInfo = '';
this.loadedSize = 0;
this.totalSize = 0;
this.pdfUrl = '';
this.downloadProgress = 0;
}
},
beforeUnmount() {
this.resetViewer();
}
};
</script>
<style scoped>
.pdf-viewer-container {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.control-panel {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
flex-wrap: wrap;
}
.control-panel button {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.control-panel button:hover:not(:disabled) {
background: #e9e9e9;
}
.control-panel button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.page-info {
margin-left: auto;
font-weight: bold;
color: #333;
}
.loading-container {
text-align: center;
padding: 40px;
}
.progress-info {
max-width: 400px;
margin: 0 auto;
}
.progress-bar {
width: 100%;
height: 20px;
background: #e0e0e0;
border-radius: 10px;
margin-top: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #4CAF50;
transition: width 0.3s ease;
}
.error-message {
background: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
border-left: 4px solid #c62828;
}
.pdf-container {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: white;
min-height: 600px;
display: flex;
flex-direction: column;
align-items: center;
}
.pdf-container.hidden {
display: none;
}
.no-pdf-message {
color: #666;
font-style: italic;
text-align: center;
margin-top: 50px;
}
.debug-info {
margin-top: 20px;
padding: 10px;
background: #e3f2fd;
border-radius: 4px;
font-size: 12px;
color: #1565c0;
}
</style>
\ No newline at end of file
......@@ -76,8 +76,8 @@
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
v-if="(userStore.isSuperAdmin === 0 && scope.row.scope !== 1) || userStore.isSuperAdmin === 1">修改</el-button>
<!-- <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']">新增</el-button>-->
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
v-if="(userStore.isSuperAdmin === 0 && scope.row.scope !== 1) || userStore.isSuperAdmin === 1">删除</el-button>
<!-- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
v-if="(userStore.isSuperAdmin === 0 && scope.row.scope !== 1) || userStore.isSuperAdmin === 1">删除</el-button> -->
</template>
</el-table-column>
</el-table>
......
......@@ -60,6 +60,11 @@
</template>
</el-table-column>
<el-table-column label="项目名称" prop="projectName" />
<el-table-column prop="projectType" label="项目类型">
<template #default="scope">
<dict-tag :options="sys_project_type" :value="scope.row.projectType" />
</template>
</el-table-column>
<el-table-column prop="scope" label="作用域">
<template #default="scope">
<dict-tag :options="sys_scope" :value="scope.row.scope" />
......@@ -129,7 +134,7 @@
"
>修改</el-button
>
<el-button
<!-- <el-button
link
type="primary"
icon="Delete"
......@@ -139,7 +144,7 @@
userStore.isSuperAdmin === 1
"
>删除</el-button
>
> -->
</template>
</el-table-column>
</el-table>
......@@ -158,6 +163,18 @@
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
<!-- 项目类型选择 -->
<el-form-item label="项目类型">
<el-radio-group v-model="form.projectType">
<el-radio
v-for="dict in sys_project_type"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="项目开始时间" prop="startTime">
<el-date-picker
v-model="form.startTime"
......@@ -261,7 +278,7 @@
</el-radio-group>
</el-form-item>
<el-form-item
label="项目图标"
label="项目图标"0
prop="logoUrl"
:rules="[{ required: true, message: '请上传项目图标', trigger: 'change' }]"
>
......@@ -363,7 +380,7 @@ import ImageUpload from '@/components/ImageUpload/index.vue' //图片上传组
const userStore = useUserStore()
const router = useRouter()
const { proxy } = getCurrentInstance()
const { sys_status, sys_scope, sys_no_yes } = proxy.useDict('sys_status', 'sys_scope', 'sys_no_yes')
const { sys_status, sys_scope, sys_no_yes,sys_project_type } = proxy.useDict('sys_status', 'sys_scope', 'sys_no_yes','sys_project_type')
const projectList = ref([])
const open = ref(false)
......
<template>
<div class="app-container">
<el-button type="primary" icon="Back" @click="handleBack" style="margin-bottom: 10px"
>返回</el-button
>
<!-- 选项卡组件:增加 custom-tabs 类名 -->
<el-tabs v-model="activeTab" type="card" @tab-change="handleTabChange" class="custom-tabs">
<!-- 用户权限选项卡 -->
......@@ -620,7 +623,9 @@ import {
import { ref } from 'vue'
import useUserStore from '@/store/modules/user'
import { ElMessage } from 'element-plus'
const router = useRouter()
const userStore = useUserStore()
const { proxy } = getCurrentInstance()
const {
......@@ -848,7 +853,7 @@ function importProjectUserListSubmitForm() {
proxy.$modal
.confirm('是否确认导入用户账号为"' + importProjectUserListNameList + '"的数据项?')
.then(function () {
return addImportProjectUserList(importProjectUserListIdList, projectBizId,tenantBizId)
return addImportProjectUserList(importProjectUserListIdList, projectBizId, tenantBizId)
})
.then(() => {
importProjectUserListOpen.value = false
......@@ -943,7 +948,7 @@ function importProjectRoleListSubmitForm() {
proxy.$modal
.confirm('是否确认导入角色名称为"' + importProjectRoleListNameList + '"的数据项?')
.then(function () {
return addImportProjectRoleList(importProjectRoleListIdList, projectBizId,tenantBizId)
return addImportProjectRoleList(importProjectRoleListIdList, projectBizId, tenantBizId)
})
.then(() => {
importProjectRoleListOpen.value = false
......@@ -998,7 +1003,7 @@ function handleImportProjectMenuList() {
// 加载菜单树
const loadMenuTree = async () => {
try {
const res = await getMenuTree(route.query.projectBizId,route.query.tenantBizId)
const res = await getMenuTree(route.query.projectBizId, route.query.tenantBizId)
menuTree.value = res.data // 直接使用后端返回的树形结构
loadImportSelectedMenuList() // 加载选中的菜单列表,更新树勾选
} catch (error) {
......@@ -1009,7 +1014,7 @@ const loadMenuTree = async () => {
// 修改加载选中菜单列表的逻辑
const loadImportSelectedMenuList = async () => {
try {
const res = await getImportSelectedMenuList(route.query.projectBizId,route.query.tenantBizId)
const res = await getImportSelectedMenuList(route.query.projectBizId, route.query.tenantBizId)
const targetKeys = res.data || []
// 开启严格模式(禁用联动)
......@@ -1368,7 +1373,9 @@ const handleTabChange = tabName => {
getMenuList()
}
}
const handleBack = () => {
router.go(-1)
}
getUserList()
</script>
......
......@@ -90,8 +90,8 @@
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
v-if="(userStore.isSuperAdmin === 0 && scope.row.scope !== 1) || userStore.isSuperAdmin === 1">修改</el-button>
<el-button link type="primary" icon="Delete"
v-if="(userStore.isSuperAdmin === 0 && scope.row.scope !== 1) || userStore.isSuperAdmin === 1">删除</el-button>
<!-- <el-button link type="primary" icon="Delete"
v-if="(userStore.isSuperAdmin === 0 && scope.row.scope !== 1) || userStore.isSuperAdmin === 1">删除</el-button> -->
</template>
</el-table-column>
</el-table>
......
......@@ -267,7 +267,6 @@ import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import { ref } from 'vue'
const props = defineProps({
tenantBizId: {
type: String,
......@@ -534,7 +533,7 @@ const getDeptTree = () => {
deptBizIdValue.value = response.data[0].deptBizId
deptOptions.value = response.data
enabledDeptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
getList()
// getList()
// 等待 DOM 更新后选中第一个节点
nextTick(() => {
selectFirstTreeNode()
......@@ -638,7 +637,10 @@ watch(
newTab => {
if (newTab == 'dept') {
getDeptTree()
resetQuery()
dateRange.value = []
proxy.resetForm('queryRef')
queryParams.value.deptId = undefined
proxy.$refs.deptTreeRef.setCurrentKey(null)
}
},
{ immediate: true }
......
......@@ -63,7 +63,7 @@
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
<el-button v-if="userStore.isSuperAdmin === 1" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
</el-form-item>
</el-form>
......@@ -124,9 +124,9 @@
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
>修改</el-button
>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
<!-- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
>删除</el-button
>
> -->
</template>
</el-table-column>
</el-table>
......
<template>
<div class="app-container">
<el-button type="primary" icon="Back" @click="handleBack" style="margin-bottom: 10px"
>返回</el-button
>
<!-- 选项卡组件:增加 custom-tabs 类名 -->
<el-tabs v-model="activeTab" type="card" @tab-change="handleTabChange" class="custom-tabs">
<!-- 项目权限选项卡 -->
......@@ -45,19 +48,19 @@
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
link
type="primary"
icon="Edit"
@click="handlePermission(scope.row)"
v-if="
(userStore.isSuperAdmin === 0 && scope.row.scope !== 1) ||
userStore.isSuperAdmin === 1
"
>分配权限</el-button
link
type="primary"
icon="Edit"
@click="handlePermission(scope.row)"
v-if="
(userStore.isSuperAdmin === 0 && scope.row.scope !== 1) ||
userStore.isSuperAdmin === 1
"
>分配权限</el-button
>
<!-- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"-->
<!-- >删除</el-button-->
<!-- >-->
<!-- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"-->
<!-- >删除</el-button-->
<!-- >-->
</template>
</el-table-column>
</el-table>
......@@ -938,7 +941,12 @@ function handleDelete(row) {
/** 项目分配权限 */
function handlePermission(row) {
router.push(`/system/project/permission?tenantBizId=`+route.query.tenantBizId+`&projectBizId=` + row.projectBizId)
router.push(
`/system/project/permission?tenantBizId=` +
route.query.tenantBizId +
`&projectBizId=` +
row.projectBizId
)
}
//========项目-列表结束=========
......@@ -1619,7 +1627,9 @@ const handleTabChange = tabName => {
getMenuList()
}
}
const handleBack = () => {
router.go(-1)
}
getList()
</script>
......
<template>
<div class="app-container">
<h4 class="form-header h4">基本信息</h4>
<el-form :model="form" label-width="80px">
<el-row>
<el-col :span="8" :offset="2">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" disabled />
</el-form-item>
</el-col>
<el-col :span="8" :offset="2">
<el-form-item label="登录账号" prop="userName">
<el-input v-model="form.userName" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
<h4 class="form-header h4">角色信息</h4>
<el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="roleRef" @selection-change="handleSelectionChange" :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)">
<el-table-column label="序号" width="55" type="index" align="center">
<template #default="scope">
<span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55"></el-table-column>
<el-table-column label="角色编号" align="center" prop="roleId" />
<el-table-column label="角色名称" align="center" prop="roleName" />
<el-table-column label="权限字符" align="center" prop="roleKey" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" />
<el-form label-width="100px">
<div style="text-align: center;margin-left:-120px;margin-top:30px;">
<el-button type="primary" @click="submitForm()">提交</el-button>
<el-button @click="close()">返回</el-button>
</div>
</el-form>
</div>
</template>
<script setup name="AuthRole">
import { getAuthRole, updateAuthRole } from "@/api/system/user"
const route = useRoute()
const { proxy } = getCurrentInstance()
const loading = ref(true)
const total = ref(0)
const pageNum = ref(1)
const pageSize = ref(10)
const roleIds = ref([])
const roles = ref([])
const form = ref({
nickName: undefined,
userName: undefined,
userId: undefined
})
/** 单击选中行数据 */
function clickRow(row) {
if (checkSelectable(row)) {
proxy.$refs["roleRef"].toggleRowSelection(row)
}
}
/** 多选框选中数据 */
function handleSelectionChange(selection) {
roleIds.value = selection.map(item => item.roleId)
}
/** 保存选中的数据编号 */
function getRowKey(row) {
return row.roleId
}
// 检查角色状态
function checkSelectable(row) {
return row.status === "0" ? true : false
}
/** 关闭按钮 */
function close() {
const obj = { path: "/system/user" }
proxy.$tab.closeOpenPage(obj)
}
/** 提交按钮 */
function submitForm() {
const userId = form.value.userId
const rIds = roleIds.value.join(",")
updateAuthRole({ userId: userId, roleIds: rIds }).then(response => {
proxy.$modal.msgSuccess("授权成功")
close()
})
}
(() => {
const userId = route.params && route.params.userId
if (userId) {
loading.value = true
getAuthRole(userId).then(response => {
form.value = response.user
roles.value = response.roles
total.value = roles.value.length
nextTick(() => {
roles.value.forEach(row => {
if (row.flag) {
proxy.$refs["roleRef"].toggleRowSelection(row)
}
})
})
loading.value = false
})
}
})()
</script>
<template>
<div class="app-container">
<el-row :gutter="20">
<splitpanes :horizontal="appStore.device === 'mobile'" class="default-theme">
<!--部门数据-->
<pane size="16">
<el-col>
<div class="head-container">
<el-input v-model="deptName" placeholder="请输入部门名称" clearable prefix-icon="Search" style="margin-bottom: 20px" />
</div>
<div class="head-container">
<el-tree :data="deptOptions" :props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" ref="deptTreeRef" node-key="id" highlight-current default-expand-all @node-click="handleNodeClick" />
</div>
</el-col>
</pane>
<!--用户数据-->
<pane size="84">
<el-col>
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="用户名称" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="用户状态" clearable style="width: 240px">
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" style="width: 308px">
<el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:user:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate" v-hasPermi="['system:user:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:user:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['system:user:import']">导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:user:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns.userId.visible" />
<el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if="columns.userName.visible" :show-overflow-tooltip="true" />
<el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns.nickName.visible" :show-overflow-tooltip="true" />
<el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns.deptName.visible" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns.phonenumber.visible" width="120" />
<el-table-column label="状态" align="center" key="status" v-if="columns.status.visible">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="0"
inactive-value="1"
@change="handleStatusChange(scope.row)"
></el-switch>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" v-if="columns.createTime.visible" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top" v-if="scope.row.userId !== 1">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top" v-if="scope.row.userId !== 1">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']"></el-button>
</el-tooltip>
<el-tooltip content="重置密码" placement="top" v-if="scope.row.userId !== 1">
<el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPwd']"></el-button>
</el-tooltip>
<el-tooltip content="分配角色" placement="top" v-if="scope.row.userId !== 1">
<el-button link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-col>
</pane>
</splitpanes>
</el-row>
<!-- 添加或修改用户配置对话框 -->
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form :model="form" :rules="rules" ref="userRef" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="归属部门" prop="deptId">
<el-tree-select v-model="form.deptId" :data="enabledDeptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择归属部门" check-strictly />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="用户性别">
<el-select v-model="form.sex" placeholder="请选择">
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="岗位">
<el-select v-model="form.postIds" multiple placeholder="请选择">
<el-option v-for="item in postOptions" :key="item.postId" :label="item.postName" :value="item.postId" :disabled="item.status == 1"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="角色">
<el-select v-model="form.roleIds" multiple placeholder="请选择">
<el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId" :disabled="item.status == 1"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 用户导入对话框 -->
<el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
<el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
</div>
<span>仅允许导入xls、xlsx格式文件。</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="User">
import { getToken } from "@/utils/auth"
import useAppStore from '@/store/modules/app'
import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect } from "@/api/system/user"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
const router = useRouter()
const appStore = useAppStore()
const { proxy } = getCurrentInstance()
const { sys_normal_disable, sys_user_sex } = proxy.useDict("sys_normal_disable", "sys_user_sex")
const userList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref("")
const dateRange = ref([])
const deptName = ref("")
const deptOptions = ref(undefined)
const enabledDeptOptions = ref(undefined)
const initPassword = ref(undefined)
const postOptions = ref([])
const roleOptions = ref([])
/*** 用户导入参数 */
const upload = reactive({
// 是否显示弹出层(用户导入)
open: false,
// 弹出层标题(用户导入)
title: "",
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: { Authorization: "Bearer " + getToken() },
// 上传的地址
url: import.meta.env.VITE_APP_BASE_API + "/system/user/importData"
})
// 列显隐信息
const columns = ref({
userId: { label: '用户编号', visible: true },
userName: { label: '用户名称', visible: true },
nickName: { label: '用户昵称', visible: true },
deptName: { label: '部门', visible: true },
phonenumber: { label: '手机号码', visible: true },
status: { label: '状态', visible: true },
createTime: { label: '创建时间', visible: true }
})
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
userName: undefined,
phonenumber: undefined,
status: undefined,
deptId: undefined
},
rules: {
userName: [{ required: true, message: "用户名称不能为空", trigger: "blur" }, { min: 2, max: 20, message: "用户名称长度必须介于 2 和 20 之间", trigger: "blur" }],
nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
password: [{ required: true, message: "用户密码不能为空", trigger: "blur" }, { min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }],
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
}
})
const { queryParams, form, rules } = toRefs(data)
/** 通过条件过滤节点 */
const filterNode = (value, data) => {
if (!value) return true
return data.label.indexOf(value) !== -1
}
/** 根据名称筛选部门树 */
watch(deptName, val => {
proxy.$refs["deptTreeRef"].filter(val)
})
/** 查询用户列表 */
function getList() {
loading.value = true
listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => {
loading.value = false
userList.value = res.rows
total.value = res.total
})
}
/** 查询部门下拉树结构 */
function getDeptTree() {
deptTreeSelect().then(response => {
deptOptions.value = response.data
enabledDeptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
})
}
/** 过滤禁用的部门 */
function filterDisabledDept(deptList) {
return deptList.filter(dept => {
if (dept.disabled) {
return false
}
if (dept.children && dept.children.length) {
dept.children = filterDisabledDept(dept.children)
}
return true
})
}
/** 节点单击事件 */
function handleNodeClick(data) {
queryParams.value.deptId = data.id
handleQuery()
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1
getList()
}
/** 重置按钮操作 */
function resetQuery() {
dateRange.value = []
proxy.resetForm("queryRef")
queryParams.value.deptId = undefined
proxy.$refs.deptTreeRef.setCurrentKey(null)
handleQuery()
}
/** 删除按钮操作 */
function handleDelete(row) {
const userIds = row.userId || ids.value
proxy.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?').then(function () {
return delUser(userIds)
}).then(() => {
getList()
proxy.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
/** 导出按钮操作 */
function handleExport() {
proxy.download("system/user/export", {
...queryParams.value,
},`user_${new Date().getTime()}.xlsx`)
}
/** 用户状态修改 */
function handleStatusChange(row) {
let text = row.status === "0" ? "启用" : "停用"
proxy.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?').then(function () {
return changeUserStatus(row.userId, row.status)
}).then(() => {
proxy.$modal.msgSuccess(text + "成功")
}).catch(function () {
row.status = row.status === "0" ? "1" : "0"
})
}
/** 更多操作 */
function handleCommand(command, row) {
switch (command) {
case "handleResetPwd":
handleResetPwd(row)
break
case "handleAuthRole":
handleAuthRole(row)
break
default:
break
}
}
/** 跳转角色分配 */
function handleAuthRole(row) {
const userId = row.userId
router.push("/system/user-auth/role/" + userId)
}
/** 重置密码按钮操作 */
function handleResetPwd(row) {
proxy.$prompt('请输入"' + row.userName + '"的新密码', "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
closeOnClickModal: false,
inputPattern: /^.{5,20}$/,
inputErrorMessage: "用户密码长度必须介于 5 和 20 之间",
inputValidator: (value) => {
if (/<|>|"|'|\||\\/.test(value)) {
return "不能包含非法字符:< > \" ' \\\ |"
}
},
}).then(({ value }) => {
resetUserPwd(row.userId, value).then(response => {
proxy.$modal.msgSuccess("修改成功,新密码是:" + value)
})
}).catch(() => {})
}
/** 选择条数 */
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.userId)
single.value = selection.length != 1
multiple.value = !selection.length
}
/** 导入按钮操作 */
function handleImport() {
upload.title = "用户导入"
upload.open = true
}
/** 下载模板操作 */
function importTemplate() {
proxy.download("system/user/importTemplate", {
}, `user_template_${new Date().getTime()}.xlsx`)
}
/**文件上传中处理 */
const handleFileUploadProgress = (event, file, fileList) => {
upload.isUploading = true
}
/** 文件上传成功处理 */
const handleFileSuccess = (response, file, fileList) => {
upload.open = false
upload.isUploading = false
proxy.$refs["uploadRef"].handleRemove(file)
proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
getList()
}
/** 提交上传文件 */
function submitFileForm() {
proxy.$refs["uploadRef"].submit()
}
/** 重置操作表单 */
function reset() {
form.value = {
userId: undefined,
deptId: undefined,
userName: undefined,
nickName: undefined,
password: undefined,
phonenumber: undefined,
email: undefined,
sex: undefined,
status: "0",
remark: undefined,
postIds: [],
roleIds: []
}
proxy.resetForm("userRef")
}
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
/** 新增按钮操作 */
function handleAdd() {
reset()
getUser().then(response => {
postOptions.value = response.posts
roleOptions.value = response.roles
open.value = true
title.value = "添加用户"
form.value.password = initPassword.value
})
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset()
const userId = row.userId || ids.value
getUser(userId).then(response => {
form.value = response.data
postOptions.value = response.posts
roleOptions.value = response.roles
form.value.postIds = response.postIds
form.value.roleIds = response.roleIds
open.value = true
title.value = "修改用户"
form.password = ""
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["userRef"].validate(valid => {
if (valid) {
if (form.value.userId != undefined) {
updateUser(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功")
open.value = false
getList()
})
} else {
addUser(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功")
open.value = false
getList()
})
}
}
})
}
onMounted(() => {
getDeptTree()
getList()
proxy.getConfigKey("sys.user.initPassword").then(response => {
initPassword.value = response.msg
})
})
</script>
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="6" :xs="24">
<el-card class="box-card">
<template v-slot:header>
<div class="clearfix">
<span>个人信息</span>
</div>
</template>
<div>
<div class="text-center">
<userAvatar />
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">
<svg-icon icon-class="user" />用户名称
<div class="pull-right">{{ state.user.userName }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="phone" />手机号码
<div class="pull-right">{{ state.user.phonenumber }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="email" />用户邮箱
<div class="pull-right">{{ state.user.email }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="tree" />所属部门
<div class="pull-right" v-if="state.user.dept">{{ state.user.dept.deptName }} / {{ state.postGroup }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="peoples" />所属角色
<div class="pull-right">{{ state.roleGroup }}</div>
</li>
<li class="list-group-item">
<svg-icon icon-class="date" />创建日期
<div class="pull-right">{{ state.user.createTime }}</div>
</li>
</ul>
</div>
</el-card>
</el-col>
<el-col :span="18" :xs="24">
<el-card>
<template v-slot:header>
<div class="clearfix">
<span>基本资料</span>
</div>
</template>
<el-tabs v-model="selectedTab">
<el-tab-pane label="基本资料" name="userinfo">
<userInfo :user="state.user" />
</el-tab-pane>
<el-tab-pane label="修改密码" name="resetPwd">
<resetPwd />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup name="Profile">
import userAvatar from "./userAvatar"
import userInfo from "./userInfo"
import resetPwd from "./resetPwd"
import { getUserProfile } from "@/api/system/user"
const route = useRoute()
const selectedTab = ref("userinfo")
const state = reactive({
user: {},
roleGroup: {},
postGroup: {}
})
function getUser() {
getUserProfile().then(response => {
state.user = response.data
state.roleGroup = response.roleGroup
state.postGroup = response.postGroup
})
}
onMounted(() => {
const activeTab = route.params && route.params.activeTab
if (activeTab) {
selectedTab.value = activeTab
}
getUser()
})
</script>
<template>
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">保存</el-button>
<el-button type="danger" @click="close">关闭</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { updateUserPwd } from "@/api/system/user"
const { proxy } = getCurrentInstance()
const user = reactive({
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
})
const equalToPassword = (rule, value, callback) => {
if (user.newPassword !== value) {
callback(new Error("两次输入的密码不一致"))
} else {
callback()
}
}
const rules = ref({
oldPassword: [{ required: true, message: "旧密码不能为空", trigger: "blur" }],
newPassword: [{ required: true, message: "新密码不能为空", trigger: "blur" }, { min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }],
confirmPassword: [{ required: true, message: "确认密码不能为空", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" }]
})
/** 提交按钮 */
function submit() {
proxy.$refs.pwdRef.validate(valid => {
if (valid) {
updateUserPwd(user.oldPassword, user.newPassword).then(response => {
proxy.$modal.msgSuccess("修改成功")
})
}
})
}
/** 关闭按钮 */
function close() {
proxy.$tab.closePage()
}
</script>
<template>
<div class="user-info-head" @click="editCropper()">
<img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
<el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<vue-cropper
ref="cropper"
:img="options.img"
:info="true"
:autoCrop="options.autoCrop"
:autoCropWidth="options.autoCropWidth"
:autoCropHeight="options.autoCropHeight"
:fixedBox="options.fixedBox"
:outputType="options.outputType"
@realTime="realTime"
v-if="visible"
/>
</el-col>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
<div class="avatar-upload-preview">
<img :src="options.previews.url" :style="options.previews.img" />
</div>
</el-col>
</el-row>
<br />
<el-row>
<el-col :lg="2" :md="2">
<el-upload
action="#"
:http-request="requestUpload"
:show-file-list="false"
:before-upload="beforeUpload"
>
<el-button>
选择
<el-icon class="el-icon--right"><Upload /></el-icon>
</el-button>
</el-upload>
</el-col>
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
<el-button icon="Plus" @click="changeScale(1)"></el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="Minus" @click="changeScale(-1)"></el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
</el-col>
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
</el-col>
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
<el-button type="primary" @click="uploadImg()">提 交</el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script setup>
import "vue-cropper/dist/index.css"
import { VueCropper } from "vue-cropper"
import { uploadAvatar } from "@/api/system/user"
import useUserStore from "@/store/modules/user"
const userStore = useUserStore()
const { proxy } = getCurrentInstance()
const open = ref(false)
const visible = ref(false)
const title = ref("修改头像")
//图片裁剪数据
const options = reactive({
img: userStore.avatar, // 裁剪图片的地址
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true, // 固定截图框大小 不允许改变
outputType: "png", // 默认生成截图为PNG格式
filename: 'avatar', // 文件名称
previews: {} //预览数据
})
/** 编辑头像 */
function editCropper() {
open.value = true
}
/** 打开弹出层结束时的回调 */
function modalOpened() {
visible.value = true
}
/** 覆盖默认上传行为 */
function requestUpload() {}
/** 向左旋转 */
function rotateLeft() {
proxy.$refs.cropper.rotateLeft()
}
/** 向右旋转 */
function rotateRight() {
proxy.$refs.cropper.rotateRight()
}
/** 图片缩放 */
function changeScale(num) {
num = num || 1
proxy.$refs.cropper.changeScale(num)
}
/** 上传预处理 */
function beforeUpload(file) {
if (file.type.indexOf("image/") == -1) {
proxy.$modal.msgError("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。")
} else {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
options.img = reader.result
options.filename = file.name
}
}
}
/** 上传图片 */
function uploadImg() {
proxy.$refs.cropper.getCropBlob(data => {
let formData = new FormData()
formData.append("avatarfile", data, options.filename)
uploadAvatar(formData).then(response => {
open.value = false
options.img = import.meta.env.VITE_APP_BASE_API + response.imgUrl
userStore.avatar = options.img
proxy.$modal.msgSuccess("修改成功")
visible.value = false
})
})
}
/** 实时预览 */
function realTime(data) {
options.previews = data
}
/** 关闭窗口 */
function closeDialog() {
options.img = userStore.avatar
options.visible = false
}
</script>
<style lang='scss' scoped>
.user-info-head {
position: relative;
display: inline-block;
height: 120px;
}
.user-info-head:hover:after {
content: "+";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #eee;
background: rgba(0, 0, 0, 0.5);
font-size: 24px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
cursor: pointer;
line-height: 110px;
border-radius: 50%;
}
</style>
\ No newline at end of file
<template>
<el-form ref="userRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户昵称" prop="nickName">
<el-input v-model="form.nickName" maxlength="30" />
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" maxlength="50" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="form.sex">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">保存</el-button>
<el-button type="danger" @click="close">关闭</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { updateUserProfile } from "@/api/system/user"
const props = defineProps({
user: {
type: Object
}
})
const { proxy } = getCurrentInstance()
const form = ref({})
const rules = ref({
nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
email: [{ required: true, message: "邮箱地址不能为空", trigger: "blur" }, { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
phonenumber: [{ required: true, message: "手机号码不能为空", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
})
/** 提交按钮 */
function submit() {
proxy.$refs.userRef.validate(valid => {
if (valid) {
updateUserProfile(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功")
props.user.phonenumber = form.value.phonenumber
props.user.email = form.value.email
})
}
})
}
/** 关闭按钮 */
function close() {
proxy.$tab.closePage()
}
// 回显当前登录用户信息
watch(() => props.user, user => {
if (user) {
form.value = { nickName: user.nickName, phonenumber: user.phonenumber, email: user.email, sex: user.sex }
}
},{ immediate: true })
</script>
......@@ -325,6 +325,7 @@ function handleStatusChange(row, event) {
/** 重置新增的表单以及其他数据 */
function reset() {
form.value = {
loginTenantBizId: userStore.currentTenant.apiLoginTenantInfoResponse.tenantBizId,
userBizId: undefined,
userName: undefined,
password: undefined,
......
......@@ -13,7 +13,9 @@ import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { getVisitPermission } from '@/api/common'
import { getToken } from '@/utils/auth'
import useUserStore from '@/store/modules/user'
const userStore = useUserStore()
const props = defineProps({
project: {
type: Object,
......@@ -26,7 +28,10 @@ const appName = computed(() => props.project.projectName)
const handleClick = () => {
console.log('点击了应用卡片', props.project)
getVisitPermission(props.project.projectBizId).then(response => {
getVisitPermission(
props.project.projectBizId,
userStore.currentTenant.apiLoginTenantInfoResponse.tenantBizId
).then(response => {
if (response.code === 200) {
// 有权限访问项目,进行跳转
// 记录最近使用的应用
......
......@@ -9,11 +9,7 @@
</template>
<div class="app-list">
<AppCard
v-for="project in projects"
:key="project.projectBizId"
:project="project"
/>
<AppCard v-for="project in projects" :key="project.projectBizId" :project="project" />
</div>
<div v-if="!projects.length" class="empty-container">
......@@ -42,11 +38,12 @@ const projects = computed(() => {
// 监听租户变化
watch(
() => userStore.currentTenant,
() => {
// 租户变化时自动刷新
}
() => userStore.currentTenant,
() => {
// 租户变化时自动刷新
}
)
userStore.getInfo()
</script>
<style lang="scss" scoped>
......
......@@ -6,74 +6,74 @@ const baseUrl = 'http://localhost:8080' // 后端接口
// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd())
const { VITE_APP_ENV } = env
return {
// 部署生产环境和开发环境下的URL。
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
base: VITE_APP_ENV === 'production' ? '/' : '/',
plugins: createVitePlugins(env, command === 'build'),
resolve: {
// https://cn.vitejs.dev/config/#resolve-alias
alias: {
// 设置路径
'~': path.resolve(__dirname, './'),
// 设置别名
'@': path.resolve(__dirname, './src')
},
// https://cn.vitejs.dev/config/#resolve-extensions
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
// 打包配置
build: {
// https://vite.dev/config/build-options.html
sourcemap: command === 'build' ? false : 'inline',
outDir: 'dist',
assetsDir: 'assets',
chunkSizeWarningLimit: 2000,
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
},
// vite 相关配置
server: {
port: 80,
host: true,
open: true,
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api': {
target: baseUrl,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
const env = loadEnv(mode, process.cwd())
const { VITE_APP_ENV } = env
return {
// 部署生产环境和开发环境下的URL。
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
base: VITE_APP_ENV === 'production' ? '/' : '/',
plugins: createVitePlugins(env, command === 'build'),
resolve: {
// https://cn.vitejs.dev/config/#resolve-alias
alias: {
// 设置路径
'~': path.resolve(__dirname, './'),
// 设置别名
'@': path.resolve(__dirname, './src')
},
// https://cn.vitejs.dev/config/#resolve-extensions
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
},
// springdoc proxy
'^/v3/api-docs/(.*)': {
target: baseUrl,
changeOrigin: true,
}
}
},
css: {
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove()
// 打包配置
build: {
// https://vite.dev/config/build-options.html
sourcemap: command === 'build' ? false : 'inline',
outDir: 'dist',
assetsDir: 'assets',
chunkSizeWarningLimit: 2000,
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
},
// vite 相关配置
server: {
port: 80,
host: true,
open: true,
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
'/dev-api': {
target: baseUrl,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '')
},
// springdoc proxy
'^/v3/api-docs/(.*)': {
target: baseUrl,
changeOrigin: true,
}
}
}
}
]
}
},
css: {
postcss: {
plugins: [
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove()
}
}
}
}
]
}
}
}
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment