Compare commits

...

2 Commits

7 changed files with 115 additions and 42 deletions

View File

@@ -28,6 +28,7 @@
import { ElMessage } from 'element-plus'
import { getUrl } from '@/utils/image'
import botLogo from '@/assets/bot_logo.png'
import { useUserStore } from '@/pinia/modules/user'
const emits = defineEmits(['change', 'update:modelValue'])
@@ -47,20 +48,14 @@
}
})
// 强制启用水印功能(临时解决方案)
const forceWatermark = true
// 调试:监听 props 变化
watch(() => props.useWatermark, (newVal) => {
console.log('useWatermark prop changed to:', newVal)
}, { immediate: true })
// 调试:打印所有 props
console.log('All props:', props)
console.log('useWatermark value:', props.useWatermark)
const editorRef = shallowRef()
const valueHtml = ref('')
const userStore = useUserStore()
const toolbarConfig = {}
@@ -68,17 +63,22 @@
const createCustomUpload = () => {
return async (file, insertFn) => {
// 直接获取当前的 props.useWatermark 值
const shouldUseWatermark = props.useWatermark || forceWatermark
const shouldUseWatermark = props.useWatermark
console.log('customUpload called, useWatermark:', shouldUseWatermark)
console.log('props.useWatermark:', props.useWatermark)
console.log('forceWatermark:', forceWatermark)
// 未开启水印则直接上传原图
if (!shouldUseWatermark) {
console.log('直接上传原图,不加水印')
const formData = new FormData()
formData.append('file', file)
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', { method: 'POST', body: formData })
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
method: 'POST',
body: formData,
headers: {
'x-token': userStore.token,
'x-user-id': userStore.userInfo.ID
}
})
const res = await resp.json()
if (res.code === 0) {
const urlPath = getUrl(res.data.file.url)
@@ -89,7 +89,7 @@
return
}
try {
console.log('开始添加水印')
console.log('开始添加水印,文件:', file.name)
const watermarkedBlob = await addBottomWatermark(file, {
stripRatio: 0.18, // 水印条高度占原图高度比例
background: 'rgba(255,255,255,0.96)',
@@ -98,14 +98,18 @@
fontFamily: 'PingFang SC, Microsoft YaHei, Arial',
logo: botLogo
})
console.log('水印处理完成')
console.log('水印处理完成,新文件大小:', watermarkedBlob.size)
const newFile = new File([watermarkedBlob], file.name, { type: watermarkedBlob.type || file.type })
const formData = new FormData()
formData.append('file', newFile)
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
method: 'POST',
body: formData
body: formData,
headers: {
'x-token': userStore.token,
'x-user-id': userStore.userInfo.ID
}
})
const res = await resp.json()
if (res.code === 0) {
@@ -183,7 +187,6 @@
const config = editor.getConfig()
if (config.MENU_CONF && config.MENU_CONF.uploadImage) {
config.MENU_CONF.uploadImage.customUpload = createCustomUpload()
console.log('动态更新上传配置useWatermark:', props.useWatermark)
}
}
@@ -216,25 +219,25 @@
// 监听 useWatermark 变化,动态更新编辑器配置
watch(
() => props.useWatermark,
(newVal) => {
console.log('useWatermark changed to:', newVal)
() => {
if (editorRef.value && editorRef.value.getConfig) {
const config = editorRef.value.getConfig()
if (config.MENU_CONF && config.MENU_CONF.uploadImage) {
config.MENU_CONF.uploadImage.customUpload = createCustomUpload()
console.log('动态更新上传配置useWatermark:', newVal)
}
}
}
)
async function addBottomWatermark(file, options) {
console.log('addBottomWatermark 开始处理,文件:', file.name, '大小:', file.size)
const { stripRatio = 0.18, background = 'rgba(255,255,255,0.96)', text = '', textColor = '#333', fontFamily = 'Arial', logo } = options || {}
const img = await fileToImage(file)
const width = img.naturalWidth || img.width
const height = img.naturalHeight || img.height
const stripHeight = Math.max(60, Math.floor(height * stripRatio))
console.log('图片尺寸:', width, 'x', height, '水印条高度:', stripHeight)
const canvas = document.createElement('canvas')
canvas.width = width
@@ -280,11 +283,20 @@
const reader = new FileReader()
reader.onload = () => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = reject
img.onload = () => {
console.log('图片加载成功,尺寸:', img.naturalWidth, 'x', img.naturalHeight)
resolve(img)
}
img.onerror = (error) => {
console.error('图片加载失败:', error)
reject(error)
}
img.src = reader.result
}
reader.onerror = reject
reader.onerror = (error) => {
console.error('文件读取失败:', error)
reject(error)
}
reader.readAsDataURL(file)
})
}
@@ -293,8 +305,14 @@
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => resolve(img)
img.onerror = reject
img.onload = () => {
console.log('Logo 加载成功,尺寸:', img.naturalWidth, 'x', img.naturalHeight)
resolve(img)
}
img.onerror = (error) => {
console.error('Logo 加载失败:', error, 'src:', src)
reject(error)
}
img.src = src
})
}
@@ -302,7 +320,10 @@
function canvasToBlob(canvas, mime) {
return new Promise((resolve) => {
if (canvas.toBlob) {
canvas.toBlob((blob) => resolve(blob), mime, 0.92)
canvas.toBlob((blob) => {
console.log('Canvas 转 Blob 成功,大小:', blob.size, '类型:', blob.type)
resolve(blob)
}, mime, 0.92)
} else {
// 兼容处理
const dataURL = canvas.toDataURL(mime)
@@ -311,7 +332,9 @@
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) u8arr[n] = bstr.charCodeAt(n)
resolve(new Blob([u8arr], { type: mime }))
const blob = new Blob([u8arr], { type: mime })
console.log('Canvas 转 Blob (兼容模式) 成功,大小:', blob.size, '类型:', blob.type)
resolve(blob)
}
})
}

View File

@@ -326,11 +326,11 @@ export const USER_TABLE_CONFIG = {
slot: 'status'
},{
attrs: {
label: '创建时间',
prop: 'CreatedAt',
label: 'VIP到期时间',
prop: 'vip_expire_time',
align: 'center'
},
slot: 'CreatedAt'
slot: 'vip_expire_time'
},{
attrs: {
label: '用户类别',

View File

@@ -93,7 +93,7 @@
<el-input v-model="formData.keyword" :clearable="true" placeholder="请输入关键词" />
</el-form-item>
<el-form-item label="内容:" prop="content" >
<RichEdit v-model="formData.content"/>
<RichEdit v-model="formData.content" :useWatermark="true"/>
</el-form-item>
</el-form>
</el-drawer>

View File

@@ -18,7 +18,12 @@
<template v-if="showAllQuery">
<!-- 将需要控制显示状态的查询条件添加到此范围内 -->
<el-form-item label="分类类型">
<el-select v-model="searchInfo.isArticle" placeholder="请选择分类类型" clearable style="width: 180px">
<el-option label="文章分类" :value="1" />
<el-option label="非文章分类" :value="0" />
</el-select>
</el-form-item>
</template>
<el-form-item>
@@ -67,9 +72,12 @@
<el-table-column align="left" label="是否启用" prop="active" width="120">
<template #default="scope">{{ formatBoolean(scope.row.active) }}</template>
</el-table-column>
<el-table-column align="left" label="首页展示" prop="active" width="120">
<el-table-column align="left" label="首页展示" prop="index" width="120">
<template #default="scope">{{ formatBoolean(scope.row.index) }}</template>
</el-table-column>
<el-table-column align="left" label="分类类型" prop="isArticle" width="120">
<template #default="scope">{{ scope.row.isArticle === 1 ? '文章分类' : '非文章分类' }}</template>
</el-table-column>
<el-table-column align="left" label="父ID" prop="parentId" width="120" />
<el-table-column align="left" label="操作" fixed="right" :min-width="appStore.operateMinWith">
<template #default="scope">
@@ -120,6 +128,12 @@
<el-form-item label="是否首页展示:" prop="index" >
<el-switch v-model="formData.index" active-color="#13ce66" inactive-color="#ff4949" active-text="是" inactive-text="否" clearable ></el-switch>
</el-form-item>
<el-form-item label="分类类型:" prop="isArticle" >
<el-select v-model="formData.isArticle" placeholder="请选择分类类型" style="width: 100%">
<el-option label="文章分类" :value="1" />
<el-option label="非文章分类" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="父ID:" prop="parentId" >
<el-input v-model.number="formData.parentId" :clearable="false" placeholder="请输入父ID" />
</el-form-item>
@@ -150,6 +164,9 @@
<el-descriptions-item label="首页展示">
{{ detailFrom.index }}
</el-descriptions-item>
<el-descriptions-item label="分类类型">
{{ detailFrom.isArticle === 1 ? '文章分类' : '非文章分类' }}
</el-descriptions-item>
<el-descriptions-item label="父ID">
{{ detailFrom.parentId }}
</el-descriptions-item>
@@ -197,7 +214,8 @@ const formData = ref({
active: false,
parentId: 0,
index: 0,
icon: ''
icon: '',
isArticle: 0
})
@@ -409,7 +427,8 @@ const closeDialog = () => {
active: false,
parentId: 0,
index: 0,
icon: ''
icon: '',
isArticle: 0
}
}
// 弹窗确定

View File

@@ -60,18 +60,19 @@
</el-col>
</el-row>
<!-- 文章介绍放在文章详情上方使用富文本 -->
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="文章详情" prop="content">
<RichEdit style="width: 100%" v-model="formData.content"/>
<el-form-item label="文章介绍" prop="desc">
<RichEdit style="width: 100%" v-model="formData.desc"/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="desc">
<el-input type="textarea" v-model="formData.desc" placeholder="请输入文章备注信息" />
<el-form-item label="文章详情" prop="content">
<RichEdit style="width: 100%" v-model="formData.content"/>
</el-form-item>
</el-col>
</el-row>
@@ -140,6 +141,10 @@
coverImg: [
{ required: true, message: '请上传封面', trigger: 'blur' }
],
// 文章介绍改为富文本,也做必填校验
desc: [
{ required: true, message: '请输入文章介绍', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入文章内容', trigger: 'blur' }
]

View File

@@ -102,8 +102,15 @@
<el-select v-model="formData.level" placeholder="请选择等级">
<el-option :value="1" label="Vip" />
<el-option :value="2" label="Svip" />
<el-option :value="3" label="讲师会员" />
</el-select>
</el-form-item>
<el-form-item v-if="formData.level === 3" label="讲师" prop="teacher_name">
<div style="display:flex;gap:8px;align-items:center;width:100%">
<el-input v-model="formData.teacher_name" placeholder="请选择讲师" readonly />
<el-button type="primary" @click="openTeacherDialog">选择讲师</el-button>
</div>
</el-form-item>
<el-form-item label="价格(元)" prop="price">
<el-input v-model.number="formData.price" placeholder="请输入价格(数字)" />
</el-form-item>
@@ -115,6 +122,7 @@
</el-form-item>
</el-form>
</el-drawer>
<user-choose ref="userChooseRef" title="讲师" @getRecipientInfo="onChooseTeacher" />
</div>
</template>
@@ -124,10 +132,14 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive } from 'vue'
import { useAppStore } from '@/pinia'
import { formatDate } from '@/utils/format'
import UserChoose from '@/components/userChoose/index.vue'
defineOptions({
name: 'VipList'
})
// 讲师选择组件注册
const userChooseRef = ref()
const appStore = useAppStore()
const btnLoading = ref(false)
@@ -143,7 +155,9 @@ const formData = ref({
level: undefined,
price: undefined,
expiration: undefined,
des: ''
des: '',
teacher_id: undefined,
teacher_name: ''
})
const rule = reactive({
@@ -151,9 +165,10 @@ const rule = reactive({
{ required: true, message: '请输入名称', trigger: ['blur','input'] },
{ whitespace: true, message: '不能只输入空格', trigger: ['blur','input'] }
],
level: [ { required: true, message: '请输入等级', trigger: ['blur','input'] } ],
level: [ { required: true, message: '请输入等级', trigger: ['blur','input','change'] } ],
price: [ { required: true, message: '请输入价格', trigger: ['blur','input'] } ],
expiration: [ { required: true, message: '请输入有效期', trigger: ['blur','input'] } ]
expiration: [ { required: true, message: '请输入有效期', trigger: ['blur','input'] } ],
teacher_name: [ { validator: (r,v,cb)=>{ if(formData.value.level===3 && !v){ cb(new Error('请选择讲师')); } else { cb(); } }, trigger:['change','blur'] } ]
})
const searchRule = reactive({
@@ -236,6 +251,7 @@ const type = ref('')
const formatLevel = (level) => {
if (level === 1) return 'Vip'
if (level === 2) return 'Svip'
if (level === 3) return '讲师会员'
return level
}
@@ -322,6 +338,16 @@ const enterDialog = async () => {
}
})
}
// 选择讲师
const openTeacherDialog = () => {
userChooseRef.value?.open()
}
const onChooseTeacher = (teacher) => {
if (!teacher) return
formData.value.teacher_id = teacher.ID
formData.value.teacher_name = teacher.nick_name
}
</script>
<style>

View File

@@ -42,8 +42,8 @@
:before-change="() => statusChangeBefore(row)" />
</template>
<template #CreatedAt="{ row }">
{{ formatDate(row.CreatedAt) }}
<template #vip_expire_time="{ row }">
{{ row.vip_expire_time }}
</template>
<!--user_type 1: 普通用户 2: 讲师-->