finish email api

dev
lynn 11 months ago
parent 48c9a71c19
commit b55cfd9bab

@ -0,0 +1,58 @@
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
// 获取邮件模板列表
export function getEmailTemplateList(params) {
return request({
url: '/api/admin/email-template/index',
method: 'get',
params
})
}
// 保存邮件模版(新增/编辑)
export function saveEmailTemplate(data) {
return request({
url: '/api/admin/email-template/save',
method: 'post',
data
})
}
// 删除邮件模版
export function deleteEmailTemplate(id) {
return request({
url: '/api/admin/email-template/destroy',
method: 'get',
params: {
id,
token: getToken()
}
})
}
export function uploadEmailRecord(data) {
return request({
url: '/api/admin/email-record/excel-show',
method: 'post',
data
})
}
// 保存邮件记录
export function saveEmailRecord(data) {
return request({
url: '/api/admin/email-record/save',
method: 'post',
data
})
}
// 获取邮件记录列表
export function getEmailRecordList(params) {
return request({
url: '/api/admin/email-record/index',
method: 'get',
params
})
}

@ -51,9 +51,9 @@
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="template-title">{{ template.name }}</div> <div class="template-title">{{ template.name }}</div>
<div class="template-meta"> <div class="template-meta">
<span><i class="el-icon-date"></i> {{ template.createTime }}</span> <span><i class="el-icon-date"></i> {{ template.createTime || template.updatedTime }}</span>
<span><i class="el-icon-data-line"></i> 使用{{ template.useCount }}</span> <span><i class="el-icon-data-line"></i> 使用{{ template.useCount }}</span>
<span><i class="el-icon-price-tag"></i> {{ template.variables.length }}个变量</span> <span><i class="el-icon-price-tag"></i> {{ template.variables ? template.variables.length : 0 }}个变量</span>
</div> </div>
</div> </div>
<el-dropdown @command="handleTemplateAction" trigger="click" @click.stop> <el-dropdown @command="handleTemplateAction" trigger="click" @click.stop>
@ -82,7 +82,7 @@
<div class="template-variables"> <div class="template-variables">
<strong>可用变量</strong> <strong>可用变量</strong>
<span v-for="variable in template.variables" :key="variable" class="variable-tag"> <span v-for="variable in template.variables" :key="variable" class="variable-tag">
{{ '{' + '{' + variable + '}' + '}' }} {{ '{' + variable + '}' }}
</span> </span>
</div> </div>
</div> </div>
@ -130,18 +130,28 @@
<!-- 用户列表选择区域 --> <!-- 用户列表选择区域 -->
<div class="recipient-table"> <div class="recipient-table">
<div class="table-header"> <div class="table-header" style="font-weight:600;font-size:16px;padding:12px 0 12px 16px;">收件人列表</div>
<el-checkbox v-model="selectAll" @change="toggleSelectAll"></el-checkbox> <div v-if="recipients.length === 0" style="padding: 60px 0; color: #909399; text-align: center;">
<i class="el-icon-user" style="font-size: 60px; margin-bottom: 16px;"></i>
<div style="font-size: 18px; margin-top: 8px;">暂无收件人数据</div>
<div style="font-size: 14px; color: #bfbfbf; margin-top: 4px;">请先上传Excel文件</div>
</div> </div>
<el-table <el-table
v-else
ref="recipientTable"
:data="recipients" :data="recipients"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
style="width: 100%"> style="width: 100%"
:max-height="400">
<el-table-column type="selection" width="55"></el-table-column> <el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column> <el-table-column
<el-table-column prop="email" label="邮箱"></el-table-column> v-for="field in recipientFields"
<el-table-column prop="company" label="公司"></el-table-column> :key="field"
<el-table-column prop="course" label="课程名称"></el-table-column> :prop="field"
:label="field"
:min-width="120"
show-overflow-tooltip>
</el-table-column>
</el-table> </el-table>
</div> </div>
@ -179,14 +189,18 @@
</div> </div>
<div v-if="selectedRecipients.length > 0" class="selected-preview"> <div v-if="selectedRecipients.length > 0" class="selected-preview">
<el-table :data="selectedRecipients" size="small"> <el-table :data="selectedRecipients" size="small" style="width: 100%;" :max-height="300">
<el-table-column prop="name" label="姓名"></el-table-column> <el-table-column
<el-table-column prop="email" label="邮箱"></el-table-column> v-for="field in recipientFields"
<el-table-column prop="company" label="公司"></el-table-column> :key="field"
<el-table-column prop="course" label="课程"></el-table-column> :prop="field"
:label="field"
:min-width="120"
show-overflow-tooltip>
</el-table-column>
<el-table-column label="操作" width="80"> <el-table-column label="操作" width="80">
<template #default="scope"> <template #default="scope">
<el-button size="mini" type="danger" @click="removeFromSelection(scope.row.id)"> <el-button size="mini" type="danger" @click="removeFromSelectionByIndex(scope.$index)">
<i class="el-icon-close"></i> <i class="el-icon-close"></i>
</el-button> </el-button>
</template> </template>
@ -227,11 +241,15 @@
<el-col :span="12"> <el-col :span="12">
<h5>收件人预览前3条</h5> <h5>收件人预览前3条</h5>
<div class="preview-table"> <div class="preview-table">
<el-table :data="previewRecipients" size="small"> <el-table :data="previewRecipients" size="small" :max-height="200">
<el-table-column prop="name" label="姓名"></el-table-column> <el-table-column
<el-table-column prop="email" label="邮箱"></el-table-column> v-for="field in recipientFields"
<el-table-column prop="company" label="公司"></el-table-column> :key="field"
<el-table-column prop="course" label="课程"></el-table-column> :prop="field"
:label="field"
:min-width="120"
show-overflow-tooltip>
</el-table-column>
</el-table> </el-table>
<div v-if="selectedRecipients.length > 3" class="more-info"> <div v-if="selectedRecipients.length > 3" class="more-info">
还有 {{ selectedRecipients.length - 3 }} 位收件人未显示 还有 {{ selectedRecipients.length - 3 }} 位收件人未显示
@ -243,7 +261,7 @@
<el-form-item label="邮件主题" required> <el-form-item label="邮件主题" required>
<el-input <el-input
v-model="sendForm.subject" v-model="sendForm.subject"
:placeholder="`支持变量,如:{{姓名}},欢迎参加{{课程名称}}`" :placeholder="`请输入邮件主题`"
@input="updateEmailPreview"> @input="updateEmailPreview">
</el-input> </el-input>
</el-form-item> </el-form-item>
@ -274,9 +292,17 @@
<el-col :span="12"> <el-col :span="12">
<h5>邮件内容预览</h5> <h5>邮件内容预览</h5>
<div class="email-preview-card"> <div class="email-preview-card">
<div class="preview-title">邮件预览</div> <div class="mb-3">
<div class="preview-meta">以第一位收件人为示例</div> <strong>主题</strong>{{ subjectPreview }}
<div class="preview-content" v-html="emailPreviewContent"></div> </div>
<div class="mb-3">
<strong>收件人</strong>{{ sampleUser.email }}
</div>
<hr>
<div class="mb-3">
<strong>邮件内容预览</strong>
</div>
<div class="template-preview-container" v-html="contentPreview"></div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
@ -351,9 +377,9 @@
</el-tabs> </el-tabs>
</div> </div>
<!-- 创建模板弹窗 --> <!-- 创建/编辑模板弹窗 -->
<el-dialog <el-dialog
title="创建邮件模板" :title="templateForm.id ? '编辑邮件模板' : '创建邮件模板'"
:visible.sync="showTemplateModal" :visible.sync="showTemplateModal"
width="80%" width="80%"
:before-close="handleCloseTemplateModal"> :before-close="handleCloseTemplateModal">
@ -372,20 +398,18 @@
v-for="variable in availableVariables" v-for="variable in availableVariables"
:key="variable" :key="variable"
class="variable-tag clickable" class="variable-tag clickable"
@click="insertVariable(`{{${variable}}}`)"> @click="insertVariable(`{${variable}}`)">
{{ '{' + '{' + variable + '}' + '}' }} {{ '{' + variable + '}' }}
</span> </span>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="邮件内容" required> <el-form-item label="邮件内容" required>
<el-input <my-tinymce
ref="templateContentRef"
v-model="templateForm.content" v-model="templateForm.content"
type="textarea" :height="300"
:rows="15" :toolbar="'undo redo | bold italic underline strikethrough | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent | removeformat | fullscreen'"
placeholder="请输入邮件内容,可以使用上方的变量..." :plugins="'lists fullscreen textcolor colorpicker'"
@input="updateTemplatePreview"> />
</el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@ -397,7 +421,7 @@
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="showTemplateModal = false">取消</el-button> <el-button @click="handleCloseTemplateModal"></el-button>
<el-button type="primary" @click="saveTemplate"></el-button> <el-button type="primary" @click="saveTemplate"></el-button>
</span> </span>
</template> </template>
@ -406,6 +430,11 @@
</template> </template>
<script> <script>
import { saveEmailTemplate, getEmailTemplateList, deleteEmailTemplate } from '@/api/email/index'
import { getToken } from '@/utils/auth'
import { uploadEmailRecord } from '@/api/email/index'
import { saveEmailRecord, getEmailRecordList } from '@/api/email/index'
export default { export default {
name: 'EmailManagement', name: 'EmailManagement',
data() { data() {
@ -420,6 +449,7 @@ export default {
// //
templateForm: { templateForm: {
id: null,
name: '', name: '',
description: '', description: '',
content: '' content: ''
@ -433,82 +463,35 @@ export default {
}, },
// //
emailTemplates: [ emailTemplates: [],
{
id: 1,
name: '课程通知模板',
description: '用于课程开始前的通知邮件',
content: `亲爱的{{姓名}},您好!
感谢您报名参加我们的课程以下是课程详细信息
课程名称{{课程名称}}
开始时间{{开始时间}}
上课地点{{地点}}
请您准时参加如有任何问题请随时联系我们
祝好
苏州科技商学院`,
variables: ['姓名', '课程名称', '开始时间', '地点'],
createTime: '2025-06-01 10:00',
useCount: 15
},
{
id: 2,
name: '课程反馈邀请',
description: '课程结束后邀请填写反馈问卷',
content: `{{姓名}}同学,您好!
感谢您参加{{课程名称}}课程为了不断改进我们的教学质量诚邀您填写课程反馈问卷
您的宝贵意见对我们非常重要问卷填写仅需3-5分钟
点击链接填写[问卷链接]
再次感谢您的参与
{{公司}}
苏州科技商学院`,
variables: ['姓名', '课程名称', '公司'],
createTime: '2025-06-02 14:30',
useCount: 8
}
],
// //
recipients: [], recipients: [],
recipientFields: [],
selectedRecipients: [], selectedRecipients: [],
// //
sendHistory: [ sendHistory: [
{
id: 1,
templateName: '课程通知模板',
recipientCount: 25,
successCount: 23,
failCount: 2,
status: 'completed',
sendTime: '2025-06-01 15:30',
details: '发送完成2封邮件因邮箱地址无效发送失败'
},
{
id: 2,
templateName: '课程反馈邀请',
recipientCount: 15,
successCount: 15,
failCount: 0,
status: 'completed',
sendTime: '2025-06-02 18:00',
details: '全部发送成功'
}
], ],
sendStatusList: [], sendStatusList: [],
availableVariables: ['姓名', '邮箱', '公司', '课程名称', '开始时间', '地点'] availableVariables: ['姓名', '邮箱', '公司', '课程名称', '开始时间', '地点']
} }
}, },
created() {
this.loadTemplates()
// tabhistory
if (this.activeTab === 'history') {
this.loadSendHistory()
}
},
watch: {
activeTab(val) {
if (val === 'history') {
this.loadSendHistory()
}
}
},
computed: { computed: {
selectedTemplate() { selectedTemplate() {
return this.emailTemplates.find(t => t.id === this.selectedTemplateId) return this.emailTemplates.find(t => t.id === this.selectedTemplateId)
@ -522,25 +505,16 @@ export default {
return this.selectedRecipients.slice(0, 3) return this.selectedRecipients.slice(0, 3)
}, },
emailPreviewContent() { sampleUser() {
if (!this.selectedTemplate || this.selectedRecipients.length === 0 || !this.sendForm.subject) { return this.selectedRecipients[0] || {}
return '请先完善发送设置...' },
} subjectPreview() {
if (!this.selectedTemplate || !this.sendForm.subject) return ''
const sampleUser = this.selectedRecipients[0] return this.replaceVariables(this.sendForm.subject, this.sampleUser)
const subjectPreview = this.replaceVariables(this.sendForm.subject, sampleUser) },
const contentPreview = this.replaceVariables(this.selectedTemplate.content, sampleUser) contentPreview() {
if (!this.selectedTemplate) return ''
return ` return this.replaceVariables(this.selectedTemplate.content, this.sampleUser)
<div class="mb-3">
<strong>主题</strong>${subjectPreview}
</div>
<div class="mb-3">
<strong>收件人</strong>${sampleUser.name} (${sampleUser.email})
</div>
<hr>
<div>${contentPreview.replace(/\n/g, '<br>')}</div>
`
}, },
templatePreviewContent() { templatePreviewContent() {
@ -565,6 +539,24 @@ export default {
}, },
methods: { methods: {
loadTemplates() {
getEmailTemplateList().then(res => {
// API
this.emailTemplates = (res.data || []).map(t => ({
id: t.id,
name: t.title, // APItitle
description: t.description,
content: t.content,
variables: this.parseVariables(t.var), // var
createTime: t.created_at,
updatedTime: t.updated_at,
useCount: t.email_records_count || 0 // 使email_records_count
}))
}).catch(error => {
console.error('加载模板失败:', error)
this.$message.error('加载模板失败')
})
},
// //
selectTemplate(templateId) { selectTemplate(templateId) {
this.selectedTemplateId = templateId this.selectedTemplateId = templateId
@ -587,7 +579,12 @@ export default {
editTemplate(templateId) { editTemplate(templateId) {
const template = this.emailTemplates.find(t => t.id === templateId) const template = this.emailTemplates.find(t => t.id === templateId)
if (template) { if (template) {
this.templateForm = { ...template } this.templateForm = {
id: template.id, // ID
name: template.name,
description: template.description,
content: template.content
}
this.showTemplateModal = true this.showTemplateModal = true
} }
}, },
@ -596,14 +593,17 @@ export default {
const template = this.emailTemplates.find(t => t.id === templateId) const template = this.emailTemplates.find(t => t.id === templateId)
if (template) { if (template) {
const newTemplate = { const newTemplate = {
...template, title: template.name + ' (副本)',
id: Date.now(), description: template.description,
name: template.name + ' (副本)', content: template.content
createTime: new Date().toLocaleString('zh-CN'),
useCount: 0
} }
this.emailTemplates.push(newTemplate) saveEmailTemplate(newTemplate).then(() => {
this.$message.success('模板复制成功!') this.$message.success('模板复制成功!')
this.loadTemplates()
}).catch(error => {
console.error('复制模板失败:', error)
this.$message.error('复制模板失败')
})
} }
}, },
@ -613,11 +613,10 @@ export default {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.emailTemplates = this.emailTemplates.filter(t => t.id !== templateId) deleteEmailTemplate(templateId).then(() => {
if (this.selectedTemplateId === templateId) { this.$message.success('删除成功!')
this.selectedTemplateId = null this.loadTemplates()
} })
this.$message.success('删除成功!')
}) })
}, },
@ -677,25 +676,41 @@ export default {
}, },
parseExcelFile(file) { parseExcelFile(file) {
// Excel // Excel
const loading = this.$loading({ const loading = this.$loading({
lock: true, lock: true,
text: '正在解析Excel文件...', text: '正在上传并解析Excel文件...',
spinner: 'el-icon-loading', spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)' background: 'rgba(0, 0, 0, 0.7)'
}) })
const formData = new FormData()
setTimeout(() => { formData.append('file', file)
const newUsers = [ uploadEmailRecord(formData)
{ id: Date.now() + 1, name: '新用户1', email: 'newuser1@example.com', company: '导入公司1', course: '导入课程1' }, .then(res => {
{ id: Date.now() + 2, name: '新用户2', email: 'newuser2@example.com', company: '导入公司2', course: '导入课程2' }, // res
{ id: Date.now() + 3, name: '新用户3', email: 'newuser3@example.com', company: '导入公司3', course: '导入课程3' } const newUsers = (res || []).map(item => ({
] email: item.email,
...item.var_data
this.recipients = [...this.recipients, ...newUsers] }))
loading.close() // email
this.$message.success(`成功导入 ${newUsers.length} 个用户数据!`) const allFields = new Set(this.recipientFields)
}, 2000) newUsers.forEach(user => {
Object.keys(user).forEach(key => {
if (key !== 'email') { // email
allFields.add(key)
}
})
})
this.recipientFields = Array.from(allFields)
this.recipients = [...this.recipients, ...newUsers]
this.$message.success(`成功导入 ${newUsers.length} 个用户数据!`)
})
.catch(() => {
this.$message.error('Excel导入失败请检查文件格式')
})
.finally(() => {
loading.close()
})
}, },
// //
@ -704,16 +719,22 @@ export default {
}, },
toggleSelectAll() { toggleSelectAll() {
this.$refs.recipientTable.toggleAllSelection() // ref
if (this.$refs.recipientTable && this.$refs.recipientTable.toggleAllSelection) {
this.$refs.recipientTable.toggleAllSelection()
}
}, },
removeFromSelection(userId) { removeFromSelectionByIndex(index) {
this.selectedRecipients = this.selectedRecipients.filter(user => user.id !== userId) this.selectedRecipients.splice(index, 1)
}, },
clearSelection() { clearSelection() {
this.selectedRecipients = [] this.selectedRecipients = []
this.$refs.recipientTable.clearSelection() // ref
if (this.$refs.recipientTable && this.$refs.recipientTable.clearSelection) {
this.$refs.recipientTable.clearSelection()
}
}, },
exportSelected() { exportSelected() {
@ -722,15 +743,12 @@ export default {
return return
} }
// CSV // CSV - 使
const csvData = [ const csvData = [
['姓名', '邮箱', '公司', '课程名称'], this.recipientFields, // 使
...this.selectedRecipients.map(user => [ ...this.selectedRecipients.map(user =>
user.name, this.recipientFields.map(field => user[field] || '') //
user.email, )
user.company,
user.course || ''
])
] ]
const csvContent = csvData.map(row => row.join(',')).join('\n') const csvContent = csvData.map(row => row.join(',')).join('\n')
@ -752,6 +770,7 @@ export default {
// //
handleCloseTemplateModal() { handleCloseTemplateModal() {
this.templateForm = { this.templateForm = {
id: null,
name: '', name: '',
description: '', description: '',
content: '' content: ''
@ -760,17 +779,15 @@ export default {
}, },
insertVariable(variable) { insertVariable(variable) {
const textarea = this.$refs.templateContentRef.$el.querySelector('textarea') // Tinymce
const start = textarea.selectionStart const editor = window.tinymce && window.tinymce.activeEditor
const end = textarea.selectionEnd if (editor) {
const text = this.templateForm.content editor.insertContent(variable)
editor.focus()
this.templateForm.content = text.substring(0, start) + variable + text.substring(end) } else {
//
this.$nextTick(() => { this.templateForm.content += variable
textarea.focus() }
textarea.setSelectionRange(start + variable.length, start + variable.length)
})
}, },
updateTemplatePreview() { updateTemplatePreview() {
@ -783,31 +800,39 @@ export default {
return return
} }
// //
const variables = [...new Set(this.templateForm.content.match(/\{\{([^}]+)\}\}/g) || [])].map(v => v.slice(2, -2)) const variables = this.extractVariables(this.templateForm.content)
const varString = variables.map(v => `{${v}}`).join(',')
const template = { const template = {
id: this.templateForm.id || Date.now(), id: this.templateForm.id || undefined, // id
name: this.templateForm.name, title: this.templateForm.name,
description: this.templateForm.description, description: this.templateForm.description,
content: this.templateForm.content, content: this.templateForm.content,
variables: variables, var: varString
createTime: new Date().toLocaleString('zh-CN'),
useCount: this.templateForm.useCount || 0
} }
if (this.templateForm.id) { if (this.templateForm.id) {
// //
const index = this.emailTemplates.findIndex(t => t.id === this.templateForm.id) saveEmailTemplate(template).then(() => {
this.emailTemplates.splice(index, 1, template) this.$message.success('模板更新成功!')
this.$message.success('模板更新成功!') this.loadTemplates() //
this.handleCloseTemplateModal()
}).catch(error => {
console.error('更新模板失败:', error)
this.$message.error('更新模板失败')
})
} else { } else {
// //
this.emailTemplates.push(template) saveEmailTemplate(template).then(res => {
this.$message.success('模板创建成功!') this.$message.success('模板创建成功!')
this.loadTemplates() //
this.handleCloseTemplateModal()
}).catch(error => {
console.error('创建模板失败:', error)
this.$message.error('创建模板失败')
})
} }
this.handleCloseTemplateModal()
}, },
// //
@ -830,50 +855,38 @@ export default {
type: 'warning' type: 'warning'
} }
).then(() => { ).then(() => {
this.simulateSending() this.sendEmailRecord()
}) })
}, },
simulateSending() { sendEmailRecord() {
if (!this.selectedTemplate || this.selectedRecipients.length === 0) {
this.$message.error('请先选择模板和收件人')
return
}
const params = {
subject: this.sendForm.subject,
email_template_id: this.selectedTemplate.id,
email_record_users: this.selectedRecipients.map(user => ({
email: user.email,
var_data: { ...user }
}))
}
if (this.sendForm.sendMode === 'schedule' && this.sendForm.scheduleTime) {
params.time = this.sendForm.scheduleTime
}
this.showSendProgress = true this.showSendProgress = true
this.sendProgress = 0 this.sendProgress = 0
this.sendStatusList = this.selectedRecipients.map(user => ({ saveEmailRecord(params)
...user, .then(res => {
status: 'pending' this.$message.success('邮件发送请求已提交!')
})) this.showSendProgress = false
//
let sent = 0 })
const total = this.selectedRecipients.length .catch(err => {
this.$message.error('邮件发送失败')
const sendInterval = setInterval(() => { this.showSendProgress = false
if (sent < total) { })
// /
const success = Math.random() > 0.1 // 90%
this.sendStatusList[sent].status = success ? 'sent' : 'failed'
sent++
this.sendProgress = Math.round((sent / total) * 100)
if (sent === total) {
clearInterval(sendInterval)
this.$message.success('邮件发送完成!')
//
const newRecord = {
id: Date.now(),
templateName: this.selectedTemplate.name,
recipientCount: total,
successCount: this.sendStatusList.filter(s => s.status === 'sent').length,
failCount: this.sendStatusList.filter(s => s.status === 'failed').length,
status: 'completed',
sendTime: new Date().toLocaleString('zh-CN'),
details: '发送完成'
}
this.sendHistory.unshift(newRecord)
}
}
}, 1000)
}, },
// //
@ -883,19 +896,52 @@ export default {
}, },
// //
replaceVariables(text, userData, isPreview = false) { extractVariables(content) {
let result = text if (!content) return []
.replace(/\{\{姓名\}\}/g, userData.name || '示例姓名') // {}
.replace(/\{\{邮箱\}\}/g, userData.email || 'example@email.com') const variableRegex = /\{([^}]+)\}/g
.replace(/\{\{公司\}\}/g, userData.company || '示例公司') const variables = []
.replace(/\{\{课程名称\}\}/g, userData.course || '示例课程') let match
.replace(/\{\{开始时间\}\}/g, '2025-06-15 09:00')
.replace(/\{\{地点\}\}/g, '苏州科技大学') while ((match = variableRegex.exec(content)) !== null) {
const variable = match[1].trim()
if (variable && !variables.includes(variable)) {
variables.push(variable)
}
}
return variables
},
if (isPreview) { parseVariables(varString) {
result = result.replace(/\{\{([^}]+)\}\}/g, '<span class="variable-tag">{{$1}}</span>') if (!varString) return []
// API"{},{}"
try {
return varString.split(',').map(v => v.trim().replace(/[{}]/g, ''))
} catch (error) {
console.error('解析变量失败:', error)
return []
} }
},
replaceVariables(text, userData, isPreview = false) {
// - 使Label
let result = text.replace(/\{([^}]+)\}/g, (match, varName) => {
varName = varName.trim()
// userData
if (userData[varName] !== undefined) {
return userData[varName] || ''
} else {
//
return isPreview ? match : ''
}
})
//
if (isPreview) {
result = result.replace(/\{([^}]+)\}/g, '<span class="variable-tag">{$1}</span>')
}
return result return result
}, },
@ -917,6 +963,14 @@ export default {
failed: '发送失败' failed: '发送失败'
} }
return statusMap[status] || '未知状态' return statusMap[status] || '未知状态'
},
loadSendHistory() {
getEmailRecordList().then(res => {
this.sendHistory = res.data || []
}).catch(error => {
this.$message.error('获取发送记录失败')
})
} }
} }
} }
@ -1096,6 +1150,14 @@ export default {
margin-bottom: 20px; margin-bottom: 20px;
} }
.recipient-table .el-table {
overflow-x: auto;
}
.recipient-table .el-table__body-wrapper {
overflow-x: auto;
}
.table-header { .table-header {
background: #409EFF; background: #409EFF;
color: white; color: white;
@ -1153,6 +1215,14 @@ export default {
margin-top: 15px; margin-top: 15px;
} }
.selected-preview .el-table {
overflow-x: auto;
}
.selected-preview .el-table__body-wrapper {
overflow-x: auto;
}
.empty-selection { .empty-selection {
text-align: center; text-align: center;
color: #6c757d; color: #6c757d;
@ -1178,6 +1248,14 @@ export default {
box-shadow: 0 2px 8px rgba(0,0,0,0.1); box-shadow: 0 2px 8px rgba(0,0,0,0.1);
} }
.preview-table .el-table {
overflow-x: auto;
}
.preview-table .el-table__body-wrapper {
overflow-x: auto;
}
.more-info { .more-info {
text-align: center; text-align: center;
color: #6c757d; color: #6c757d;
@ -1245,6 +1323,7 @@ export default {
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
line-height: 1.6; line-height: 1.6;
white-space: pre-wrap;
} }
.variable-tags { .variable-tags {

@ -14,19 +14,19 @@
<!-- 图书统计 --> <!-- 图书统计 -->
<div class="book-stats"> <div class="book-stats">
<div class="stat-card blue"> <div class="stat-card blue">
<div class="stat-number">1,247</div> <div class="stat-number">{{ chartData.total || 0 }}</div>
<div class="stat-label">总图书数量</div> <div class="stat-label">总图书数量</div>
</div> </div>
<div class="stat-card green"> <div class="stat-card green">
<div class="stat-number">856</div> <div class="stat-number">{{ chartData.borrowable || 0 }}</div>
<div class="stat-label">可借阅</div> <div class="stat-label">可借阅</div>
</div> </div>
<div class="stat-card orange"> <div class="stat-card orange">
<div class="stat-number">391</div> <div class="stat-number">{{ chartData.borrowed || 0 }}</div>
<div class="stat-label">已借出</div> <div class="stat-label">已借出</div>
</div> </div>
<div class="stat-card purple"> <div class="stat-card purple">
<div class="stat-number">23</div> <div class="stat-number">{{ chartData.maintaining || 0 }}</div>
<div class="stat-label">维护中</div> <div class="stat-label">维护中</div>
</div> </div>
</div> </div>
@ -35,21 +35,42 @@
<div class="search-section"> <div class="search-section">
<el-form :model="filters" inline> <el-form :model="filters" inline>
<el-form-item> <el-form-item>
<el-input v-model="filters.keyword" placeholder="搜索书名、作者、ISBN"></el-input> <el-input
v-model="filters.keyword"
placeholder="搜索书名、作者"
clearable
@clear="handleSearch"
></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-select v-model="filters.category" placeholder="全部分类" clearable> <el-select
<el-option label="技术类" value="tech"></el-option> v-model="filters.category"
<el-option label="商业类" value="business"></el-option> placeholder="全部分类"
<el-option label="管理类" value="management"></el-option> clearable
<el-option label="金融类" value="finance"></el-option> @clear="handleSearch"
@change="handleSearch"
>
<el-option label="全部分类" value=""></el-option>
<el-option
v-for="category in categoryList"
:key="category"
:label="category"
:value="category"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-select v-model="filters.status" placeholder="全部状态" clearable> <el-select
<el-option label="可借阅" value="available"></el-option> v-model="filters.status"
<el-option label="已借出" value="borrowed"></el-option> placeholder="全部状态"
<el-option label="维护中" value="maintenance"></el-option> clearable
@clear="handleSearch"
@change="handleSearch"
>
<el-option label="全部状态" value=""></el-option>
<el-option label="可借阅" value="0"></el-option>
<el-option label="已借出" value="1"></el-option>
<el-option label="维护中" value="2"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -74,42 +95,74 @@
<el-table-column type="selection" width="55"></el-table-column> <el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="封面" width="80"> <el-table-column label="封面" width="80">
<template slot-scope="scope"> <template slot-scope="scope">
<img :src="scope.row.cover" alt="图书封面" class="book-cover"> <img v-if="scope.row.cover && scope.row.cover.url" :src="scope.row.cover.url" alt="图书封面" class="book-cover">
<div v-else class="no-cover">无封面</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="图书信息" min-width="250"> <el-table-column label="图书信息" min-width="250">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="book-title">{{ scope.row.title }}</div> <div class="book-title">{{ scope.row.title || '未设置标题' }}</div>
<div class="book-author">作者{{ scope.row.author }} · 出版社{{ scope.row.publisher }} · {{ scope.row.year }}</div> <div class="book-author">
作者{{ scope.row.author || '未知' }}
<span v-if="scope.row.publisher">· {{ scope.row.publisher }}</span>
<span v-if="scope.row.publish_year">· {{ scope.row.publish_year }}</span>
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="分类" width="100"> <el-table-column label="分类" width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag :type="getCategoryTagType(scope.row.category)" size="small">{{ scope.row.categoryText }}</el-tag> <el-tag :type="getCategoryTagType(scope.row.category)" size="small">{{ scope.row.category || '未分类' }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="ISBN" width="150"> <el-table-column label="ISBN" width="150">
<template slot-scope="scope"> <template slot-scope="scope">
{{ scope.row.isbn }} {{ scope.row.isbn || '未设置' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" width="100"> <el-table-column label="状态" width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<el-tag :type="getStatusTagType(scope.row.status)" size="small">{{ scope.row.statusText }}</el-tag> <el-tag :type="getStatusTagType(scope.row.status)" size="small">{{ getStatusText(scope.row.status) }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="添加时间" width="150"> <el-table-column label="添加时间" width="150">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="time-display">{{ scope.row.addDate }}</div> <div class="time-display">{{ formatDate(scope.row.created_at) }}</div>
<div class="time-display-secondary">{{ scope.row.addTime }}</div> <div class="time-display-secondary">{{ formatTime(scope.row.created_at) }}</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="280" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="action-buttons"> <div class="action-buttons">
<el-button type="primary" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)"></el-button> <el-button type="primary" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)"></el-button>
<el-button type="info" size="mini" icon="el-icon-view" @click="handleView(scope.row)"></el-button> <el-button type="info" size="mini" icon="el-icon-view" @click="handleView(scope.row)"></el-button>
<el-button v-if="scope.row.status === 'borrowed'" type="warning" size="mini" icon="el-icon-refresh-left" @click="handleReturn(scope.row)"></el-button> <el-button
v-if="scope.row.status !== 1 && scope.row.status !== 2"
type="success"
size="mini"
icon="el-icon-reading"
@click="handleStatusChange(scope.row, 1)"
>已借阅</el-button>
<el-button
v-if="scope.row.status === 1"
type="warning"
size="mini"
icon="el-icon-refresh-left"
@click="handleStatusChange(scope.row, 0)"
>已归还</el-button>
<el-button
v-if="scope.row.status === 2"
type="success"
size="mini"
icon="el-icon-upload2"
@click="handleStatusChange(scope.row, 0)"
>上架</el-button>
<el-button
v-if="scope.row.status !== 2"
type="danger"
size="mini"
icon="el-icon-tools"
@click="handleStatusChange(scope.row, 2)"
>维护</el-button>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)"></el-button> <el-button type="danger" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)"></el-button>
</div> </div>
</template> </template>
@ -130,9 +183,9 @@
></el-pagination> ></el-pagination>
</div> </div>
<!-- 添加图书弹窗 --> <!-- 添加/编辑图书弹窗 -->
<el-dialog <el-dialog
title="添加图书" :title="isEdit ? '编辑图书' : '添加图书'"
:visible.sync="showUploadModal" :visible.sync="showUploadModal"
width="600px" width="600px"
:before-close="handleCloseModal" :before-close="handleCloseModal"
@ -171,7 +224,7 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="出版年份"> <el-form-item label="出版年份">
<el-input-number v-model="bookForm.year" :min="1900" :max="2030" placeholder="年份"></el-input-number> <el-input-number v-model="bookForm.year" :min="1949" :max="2030" placeholder="年份"></el-input-number>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@ -217,73 +270,147 @@
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button @click="showUploadModal = false">取消</el-button> <el-button @click="showUploadModal = false">取消</el-button>
<el-button type="primary" @click="saveBook"></el-button> <el-button type="primary" @click="saveBook">{{ isEdit ? '' : '' }}</el-button>
</div>
</el-dialog>
<!-- 图书详情弹窗 -->
<el-dialog
title="图书详情"
:visible.sync="showDetailModal"
width="700px"
:before-close="handleCloseDetailModal"
>
<div v-if="currentBook" class="book-detail">
<!-- 基本信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-info"></i>
基本信息
</div>
<el-row :gutter="20">
<el-col :span="12">
<div class="detail-item">
<label class="detail-label">书名</label>
<span class="detail-value">{{ currentBook.title || '未设置' }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<label class="detail-label">作者</label>
<span class="detail-value">{{ currentBook.author || '未知' }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<div class="detail-item">
<label class="detail-label">ISBN</label>
<span class="detail-value">{{ currentBook.isbn || '未设置' }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<label class="detail-label">出版社</label>
<span class="detail-value">{{ currentBook.publisher || '未设置' }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<div class="detail-item">
<label class="detail-label">出版年份</label>
<span class="detail-value">{{ currentBook.publish_year || '未设置' }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<label class="detail-label">分类</label>
<el-tag :type="getCategoryTagType(currentBook.category)" size="small">{{ currentBook.category || '未分类' }}</el-tag>
</div>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<div class="detail-item">
<label class="detail-label">图书状态</label>
<el-tag :type="getStatusTagType(currentBook.status)" size="small">{{ getStatusText(currentBook.status) }}</el-tag>
</div>
</el-col>
</el-row>
<div class="detail-item">
<label class="detail-label">图书简介</label>
<div class="detail-description">{{ currentBook.description || '暂无简介' }}</div>
</div>
</div>
<!-- 图书封面 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-picture"></i>
图书封面
</div>
<div class="cover-display">
<img v-if="currentBook.cover && currentBook.cover.url" :src="currentBook.cover.url" alt="图书封面" class="detail-cover">
<div v-else class="no-cover-detail">
<i class="el-icon-picture-outline" style="font-size: 48px; color: #d1d5db;"></i>
<div style="color: #9ca3af; margin-top: 10px;">暂无封面</div>
</div>
</div>
</div>
<!-- 其他信息 -->
<div class="form-section">
<div class="section-title">
<i class="el-icon-document"></i>
其他信息
</div>
<el-row :gutter="20">
<el-col :span="12">
<div class="detail-item">
<label class="detail-label">创建时间</label>
<span class="detail-value">{{ formatDateTime(currentBook.created_at) }}</span>
</div>
</el-col>
<el-col :span="12">
<div class="detail-item">
<label class="detail-label">更新时间</label>
<span class="detail-value">{{ formatDateTime(currentBook.updated_at) }}</span>
</div>
</el-col>
</el-row>
<div class="detail-item">
<label class="detail-label">图书ID</label>
<span class="detail-value">{{ currentBook.id }}</span>
</div>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="showDetailModal = false">关闭</el-button>
<el-button type="primary" @click="handleEdit(currentBook)"></el-button>
</div> </div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { save } from '@/api/library' import { save, index, destroy } from '@/api/library'
import { uploads } from '@/api/uploads' import { uploads } from '@/api/uploads'
export default { export default {
name: 'Library', name: 'Library',
data() { data() {
return { return {
showUploadModal: false, showUploadModal: false,
showDetailModal: false,
isEdit: false,
editBookId: null,
filters: { filters: {
keyword: '', keyword: '',
category: '', category: '',
status: '' status: ''
}, },
list: [ list: [],
{ total: 0,
id: 1,
title: '深度学习实战指南',
author: '张三',
publisher: '机械工业出版社',
year: '2023',
isbn: '978-7-111-12345-6',
category: 'tech',
categoryText: '技术类',
status: 'available',
statusText: '可借阅',
cover: 'https://via.placeholder.com/50x70/4285f4/ffffff?text=书',
addDate: '2024-01-15',
addTime: '10:30'
},
{
id: 2,
title: '创业公司股权设计',
author: '李四',
publisher: '中信出版社',
year: '2023',
isbn: '978-7-508-67890-1',
category: 'business',
categoryText: '商业类',
status: 'borrowed',
statusText: '已借出',
cover: 'https://via.placeholder.com/50x70/ff9800/ffffff?text=书',
addDate: '2024-01-14',
addTime: '15:20'
},
{
id: 3,
title: '敏捷项目管理实践',
author: '王五',
publisher: '电子工业出版社',
year: '2022',
isbn: '978-7-121-34567-8',
category: 'management',
categoryText: '管理类',
status: 'available',
statusText: '可借阅',
cover: 'https://via.placeholder.com/50x70/4caf50/ffffff?text=书',
addDate: '2024-01-13',
addTime: '09:15'
}
],
total: 1247,
listQuery: { listQuery: {
page: 1, page: 1,
limit: 10 limit: 10
@ -300,6 +427,7 @@ export default {
cover: '', cover: '',
cover_id: '' cover_id: ''
}, },
currentBook: null,
bookRules: { bookRules: {
title: [ title: [
{ required: true, message: '请输入书名', trigger: 'blur' } { required: true, message: '请输入书名', trigger: 'blur' }
@ -310,13 +438,64 @@ export default {
category: [ category: [
{ required: true, message: '请输入分类', trigger: 'blur' } { required: true, message: '请输入分类', trigger: 'blur' }
] ]
} },
chartData: {
total: 0,
borrowable: 0,
borrowed: 0,
maintaining: 0
},
categoryList: []
} }
}, },
created() {
this.getList()
},
methods: { methods: {
async getList() {
try {
const params = {
page: this.listQuery.page,
limit: this.listQuery.limit
}
// filter
let filterIndex = 0
//
if (this.filters.keyword) {
params.keyword = this.filters.keyword
}
if (this.filters.category) {
params[`filter[${filterIndex}][key]`] = 'category'
params[`filter[${filterIndex}][op]`] = 'eq'
params[`filter[${filterIndex}][value]`] = this.filters.category
filterIndex++
}
if (this.filters.status !== '') {
params[`filter[${filterIndex}][key]`] = 'status'
params[`filter[${filterIndex}][op]`] = 'eq'
params[`filter[${filterIndex}][value]`] = this.filters.status
filterIndex++
}
const res = await index(params)
// API
this.list = res.list?.data || []
this.total = res.list?.total || 0
this.listQuery.page = res.list?.current_page || 1
this.chartData = res.chart || {}
this.categoryList = res.category || []
} catch (error) {
console.error('获取图书列表失败:', error)
this.$message.error('获取图书列表失败')
}
},
handleSearch() { handleSearch() {
console.log('搜索条件:', this.filters) this.listQuery.page = 1
this.$message.success('搜索已触发') this.getList()
}, },
handleExport() { handleExport() {
console.log('导出数据') console.log('导出数据')
@ -330,42 +509,75 @@ export default {
this.multipleSelection = val this.multipleSelection = val
}, },
getCategoryTagType(category) { getCategoryTagType(category) {
const categoryMap = { // 使
tech: 'primary', return 'primary'
business: 'success',
management: 'warning',
finance: 'danger'
}
return categoryMap[category] || 'info'
}, },
getStatusTagType(status) { getStatusTagType(status) {
const statusMap = { const statusMap = {
available: 'success', 0: 'success', //
borrowed: 'warning', 1: 'warning', //
maintenance: 'danger' 2: 'danger' //
}
return statusMap[status] || 'info'
},
getStatusText(status) {
const statusText = {
0: '可借阅',
1: '已借出',
2: '维护中'
} }
return statusMap[status] return statusText[status] || '未知状态'
}, },
handleEdit(row) { handleEdit(row) {
console.log('编辑:', row.id) console.log('编辑:', row.id)
this.$message.info('跳转到编辑页面') this.isEdit = true
this.editBookId = row.id
//
this.bookForm = {
title: row.title || '',
author: row.author || '',
isbn: row.isbn || '',
publisher: row.publisher || '',
year: row.publish_year ? parseInt(row.publish_year) : null,
category: row.category || '',
description: row.description || '',
cover: row.cover && row.cover.url ? row.cover.url : '',
cover_id: row.cover_id || ''
}
this.showUploadModal = true
}, },
handleView(row) { handleView(row) {
console.log('查看详情:', row.id) console.log('查看详情:', row.id)
this.$message.info('跳转到详情页面') this.currentBook = row
this.showDetailModal = true
}, },
handleReturn(row) { handleStatusChange(row, status) {
this.$confirm('确认这本图书已归还吗?', '提示', { const statusText = {
0: '可借阅',
1: '已借阅',
2: '维护'
}
let actionText = ''
if (row.status === 2 && status === 0) {
actionText = '上架'
} else {
actionText = statusText[status]
}
this.$confirm(`确认将这本图书状态变更为"${actionText}"吗?`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(async () => {
console.log('归还图书:', row.id) try {
row.status = 'available' await save({ id: row.id, status: status })
row.statusText = '可借阅' this.$message.success('图书状态变更成功!')
this.$message.success('图书归还成功!') this.getList() //
} catch (error) {
console.error('状态变更失败:', error)
this.$message.error('状态变更失败,请重试')
}
}).catch(() => { }).catch(() => {
this.$message.info('已取消归还') this.$message.info('已取消变更')
}) })
}, },
handleDelete(row) { handleDelete(row) {
@ -373,30 +585,36 @@ export default {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(async () => {
console.log('删除图书:', row.id) try {
// API await destroy({ id: row.id })
const index = this.list.findIndex(item => item.id === row.id) this.$message.success('图书删除成功!')
if (index > -1) { this.getList() //
this.list.splice(index, 1) } catch (error) {
console.error('删除图书失败:', error)
this.$message.error('删除失败,请重试')
} }
this.$message.success('图书删除成功!')
}).catch(() => { }).catch(() => {
this.$message.info('已取消删除') this.$message.info('已取消删除')
}) })
}, },
handleSizeChange(val) { handleSizeChange(val) {
this.listQuery.limit = val this.listQuery.limit = val
console.log('每页显示条数:', val) this.getList()
}, },
handleCurrentChange(val) { handleCurrentChange(val) {
this.listQuery.page = val this.listQuery.page = val
console.log('当前页:', val) this.getList()
}, },
handleCloseModal() { handleCloseModal() {
this.resetForm() this.resetForm()
this.isEdit = false
this.editBookId = null
this.showUploadModal = false this.showUploadModal = false
}, },
handleCloseDetailModal() {
this.showDetailModal = false
},
triggerCoverUpload() { triggerCoverUpload() {
this.$refs.coverInput.click() this.$refs.coverInput.click()
}, },
@ -430,11 +648,11 @@ export default {
} }
}, },
async saveBook() { async saveBook() {
this.$refs.bookForm.validate(async (valid) => { this.$refs.bookForm.validate(async(valid) => {
if (valid) { if (valid) {
const loading = this.$loading({ const loading = this.$loading({
lock: true, lock: true,
text: '正在保存...', text: this.isEdit ? '正在更新...' : '正在保存...',
spinner: 'el-icon-loading', spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)' background: 'rgba(0, 0, 0, 0.7)'
}) })
@ -449,16 +667,24 @@ export default {
description: this.bookForm.description, description: this.bookForm.description,
cover_id: this.bookForm.cover_id cover_id: this.bookForm.cover_id
} }
console.log('params', params)
await save(params) if (this.isEdit) {
// ID
params.id = this.editBookId
await save(params) // APIsave
} else {
//
await save(params)
}
loading.close() loading.close()
this.$message.success('图书添加成功!') this.$message.success(this.isEdit ? '图书更新成功!' : '图书添加成功!')
this.showUploadModal = false this.showUploadModal = false
this.resetForm() this.resetForm()
// this.getList() //
} catch (e) { } catch (e) {
loading.close() loading.close()
this.$message.error('添加失败,请重试' + e.message) this.$message.error((this.isEdit ? '更新' : '添加') + '失败,请重试' + e.message)
} }
} else { } else {
this.$message.error('请填写必填字段') this.$message.error('请填写必填字段')
@ -479,15 +705,23 @@ export default {
cover: '', cover: '',
cover_id: '' cover_id: ''
} }
this.isEdit = false
this.editBookId = null
}, },
getCategoryText(category) { formatDate(dateString) {
const categoryMap = { if (!dateString) return ''
tech: '技术类', const date = new Date(dateString)
business: '商业类', return date.toLocaleDateString('zh-CN')
management: '管理类', },
finance: '金融类' formatTime(dateString) {
} if (!dateString) return ''
return categoryMap[category] || category || '' const date = new Date(dateString)
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
},
formatDateTime(dateString) {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleString('zh-CN')
} }
} }
} }
@ -550,9 +784,7 @@ export default {
.stat-card.purple { border-left-color: #9b59b6; } .stat-card.purple { border-left-color: #9b59b6; }
.stat-number { .stat-number {
font-size: 28px; font-size: 24px;
font-weight: bold;
color: #2c3e50;
} }
.stat-label { .stat-label {
@ -684,6 +916,81 @@ export default {
text-align: right; text-align: right;
} }
/* 详情弹窗样式 */
.book-detail {
max-height: 70vh;
overflow-y: auto;
}
.detail-item {
margin-bottom: 15px;
display: flex;
align-items: flex-start;
}
.detail-label {
font-weight: 600;
color: #2c3e50;
min-width: 80px;
margin-right: 10px;
line-height: 1.5;
}
.detail-value {
color: #374151;
line-height: 1.5;
flex: 1;
}
.detail-description {
color: #374151;
line-height: 1.6;
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
border-left: 3px solid #3498db;
margin-top: 5px;
white-space: pre-wrap;
word-break: break-word;
}
.cover-display {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.detail-cover {
max-width: 200px;
max-height: 280px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
object-fit: cover;
}
.no-cover-detail {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 200px;
height: 280px;
border: 2px dashed #d1d5db;
border-radius: 8px;
background: #f8f9fa;
}
.no-cover {
color: #9ca3af;
font-size: 12px;
text-align: center;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
border: 1px dashed #d1d5db;
}
/* Element UI 表格样式覆盖 */ /* Element UI 表格样式覆盖 */
::v-deep .el-table th { ::v-deep .el-table th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

Loading…
Cancel
Save