feat:添加admin相关接口

This commit is contained in:
2025-11-13 15:05:54 +08:00
parent 11cb40c86a
commit 067e0539ba
58 changed files with 18736 additions and 273 deletions
+431
View File
@@ -0,0 +1,431 @@
<template>
<el-dialog
v-model="visible"
title="选择头像"
width="800px"
append-to-body
@close="handleClose"
>
<div class="avatar-selector">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 用户文件列表 -->
<el-tab-pane label="用户文件" name="userFiles">
<div class="file-list-container">
<div class="file-list-header">
<h4>用户文件列表</h4>
<el-button type="primary" @click="switchToUpload" :icon="Upload">
上传新头像
</el-button>
</div>
<div class="file-grid" v-loading="loading">
<div
v-for="file in fileList"
:key="file.cover_id"
class="file-item"
:class="{ 'selected': selectedId === file.cover_id }"
@click="selectFile(file)"
>
<div class="file-preview">
<img
v-if="isImageFile(file)"
:src="file.url"
:alt="file.realName"
@error="handleImageError"
/>
<el-icon v-else class="file-icon"><Document /></el-icon>
</div>
<div class="file-info">
<p class="file-name">{{ file.realName }}</p>
<p class="file-size">{{ formatFileSize(file.size) }}</p>
</div>
</div>
</div>
<el-empty v-if="fileList.length === 0 && !loading" description="暂无文件" />
<!-- 分页 -->
<div class="pagination-container" v-if="total > 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</el-tab-pane>
<!-- 上传头像 -->
<el-tab-pane label="上传头像" name="upload">
<div class="upload-section">
<el-upload
:http-request="handleUpload"
:before-upload="beforeUpload"
:show-file-list="false"
accept="image/*"
drag
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
只能上传jpg/png文件且不超过2MB
</div>
</template>
</el-upload>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:disabled="!selectedId"
>
确定选择
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Upload, UploadFilled, Document } from '@element-plus/icons-vue'
import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
import { closeAllMessage } from '../../utils/message'
// Props
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
userId: {
type: [String, Number],
required: true
},
currentCoverId: {
type: [String, Number],
default: ''
}
})
// Emits
const emit = defineEmits(['update:modelValue', 'confirm'])
// 响应式数据
const visible = ref(false)
const activeTab = ref('userFiles')
const fileList = ref([])
const loading = ref(false)
const selectedId = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
// 监听 modelValue 变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
selectedId.value = props.currentCoverId
currentPage.value = 1
fetchFileList()
}
})
// 监听 visible 变化
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
// 获取文件列表
const fetchFileList = async () => {
if (!props.userId) return
loading.value = true
fileList.value = [] // 清空列表
try {
const res = await getFileList({
page: currentPage.value,
count: pageSize.value
})
console.log("获取文件列表:", res)
if (res.data.code === 200) {
const list = res.data.data.list || []
total.value = res.data.data.all_count || 0
// 获取每个文件的详情
for (let i = 0; i < list.length; i++) {
try {
console.log("获取文件详情:", list[i].id)
const res2 = await getFileDetail({ file_id: list[i].id })
if (res2.data.code === 200) {
fileList.value.push({
url: res2.data.data.url,
cover_id: res2.data.data.data.id,
size: res2.data.data.data.size,
realName: res2.data.data.data.realName
})
}
} catch (error) {
console.error('获取文件详情失败:', error)
}
}
console.log("文件列表1237", fileList.value)
}
} catch (error) {
console.error('获取文件列表失败:', error)
ElMessage.error('获取文件列表失败')
} finally {
loading.value = false
}
}
// 处理标签页切换
const handleTabClick = (tab) => {
if (tab.name === 'userFiles') {
currentPage.value = 1
fetchFileList()
}
}
// 分页处理
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchFileList()
}
const handlePageChange = (page) => {
currentPage.value = page
fetchFileList()
}
// 切换到上传标签页
const switchToUpload = () => {
activeTab.value = 'upload'
}
// 判断是否为图片文件
const isImageFile = (file) => {
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
const extension = file.realName?.split('.').pop()?.toLowerCase()
return imageTypes.includes(extension)
}
// 格式化文件大小
const formatFileSize = (size) => {
if (!size) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let unitIndex = 0
let fileSize = size
while (fileSize >= 1024 && unitIndex < units.length - 1) {
fileSize /= 1024
unitIndex++
}
return `${fileSize.toFixed(1)} ${units[unitIndex]}`
}
// 选择文件
const selectFile = (file) => {
selectedId.value = file.cover_id
}
// 上传前验证
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
}
// 自定义上传
const handleUpload = async (options) => {
const { file } = options
const formData = new FormData()
formData.append('files', file)
formData.append('file_names', file.name)
formData.append('update_type', 'cover')
try {
const res = await uploadFile(formData)
console.log("上传文件:", res)
if (res.data.code === 200) {
ElMessage.success("上传成功")
// 重置到第一页并刷新文件列表
currentPage.value = 1
await fetchFileList()
// 切换到文件列表标签页
activeTab.value = 'userFiles'
// 自动选择新上传的文件
if (res.data.data?.id) {
selectedId.value = res.data.data.id
}
} else {
ElMessage.error(res.data.msg || '上传失败')
}
} catch (error) {
console.error('上传失败:', error)
ElMessage.error('上传失败')
}
}
// 图片加载错误处理
const handleImageError = (event) => {
event.target.style.display = 'none'
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedId.value = ''
fileList.value = []
currentPage.value = 1
total.value = 0
}
// 确认选择
const handleConfirm = () => {
if (selectedId.value) {
const selectedFile = fileList.value.find(file => file.cover_id === selectedId.value)
emit('confirm', {
cover_id: selectedId.value,
url: selectedFile?.url || ''
})
handleClose()
}
}
</script>
<style scoped>
.avatar-selector {
min-height: 400px;
}
.file-list-container {
padding: 20px 0;
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.file-list-header h4 {
margin: 0;
color: #303133;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
max-height: 400px;
overflow-y: auto;
}
.file-item {
border: 2px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.file-item:hover {
border-color: #409EFF;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
.file-item.selected {
border-color: #409EFF;
background-color: #f0f9ff;
}
.file-preview {
width: 80px;
height: 80px;
margin: 0 auto 8px;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
}
.file-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-preview .file-icon {
font-size: 32px;
color: #909399;
}
.file-info {
text-align: center;
}
.file-name {
font-size: 12px;
color: #303133;
margin: 0 0 4px 0;
word-break: break-all;
line-height: 1.3;
}
.file-size {
font-size: 11px;
color: #909399;
margin: 0;
}
.upload-section {
padding: 40px 20px;
text-align: center;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
+30 -2
View File
@@ -5,7 +5,7 @@
<div class="logo-container">
<h1 class="title">零零七云计算后台控制面板</h1>
</div>
<el-scrollbar>
<el-scrollbar class="sidebar-scrollbar">
<el-menu
:default-active="activeMenu"
class="sidebar-menu"
@@ -176,9 +176,13 @@ const handleLogout = () => {
color: #1890ff;
}
.sidebar-scrollbar {
height: calc(100vh - 60px);
}
.sidebar-menu {
border-right: none;
height: calc(100vh - 60px);
min-height: 100%;
}
/* 主容器样式 */
@@ -279,4 +283,28 @@ const handleLogout = () => {
:deep(.el-dropdown-menu__item i) {
margin-right: 8px;
}
/* 侧边栏滚动条样式优化 */
:deep(.sidebar-scrollbar .el-scrollbar__wrap) {
overflow-x: hidden;
}
:deep(.sidebar-scrollbar .el-scrollbar__view) {
height: 100%;
}
/* Element Plus 菜单项样式优化 */
:deep(.el-menu) {
border-right: none;
}
:deep(.el-sub-menu__title) {
height: 48px;
line-height: 48px;
}
:deep(.el-menu-item) {
height: 48px;
line-height: 48px;
}
</style>
@@ -0,0 +1,295 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="600px"
:close-on-click-modal="false"
@close="handleClose"
>
<div v-if="detailData" class="detail-content">
<table class="detail-table">
<!-- 优惠码特有字段 -->
<tr v-if="type === 'code'">
<td class="label">优惠码</td>
<td class="value">{{ detailData.code }}</td>
</tr>
<!-- 名称 -->
<tr>
<td class="label">{{ type === 'code' ? '名称' : '代金券名称' }}</td>
<td class="value">{{ detailData.name }}</td>
</tr>
<!-- 备注 -->
<tr>
<td class="label">备注</td>
<td class="value secondary">{{ detailData.note || '无' }}</td>
</tr>
<!-- 优惠类型仅优惠码 -->
<tr v-if="type === 'code'" class="alternate">
<td class="label">优惠类型</td>
<td class="value">
<span :class="['type-tag', detailData.percentage ? 'percentage' : 'amount']">
{{ detailData.percentage ? '百分比折扣' : '固定金额' }}
</span>
</td>
</tr>
<!-- 优惠值/面额 -->
<tr :class="type === 'code' ? '' : 'alternate'">
<td class="label">{{ type === 'code' ? '优惠值' : '面额' }}</td>
<td class="value">
<span v-if="detailData.percentage" class="highlight-value percentage">
{{ (detailData.percentage / 100).toFixed(0) }}%
</span>
<span v-else class="highlight-value amount">
¥{{ (detailData.amount / 100).toFixed(2) }}
</span>
</td>
</tr>
<!-- 最低消费 -->
<tr>
<td class="label">最低消费</td>
<td class="value">¥{{ (detailData.minAmount / 100).toFixed(2) }}</td>
</tr>
<!-- 最大抵扣 -->
<tr>
<td class="label">最大抵扣</td>
<td class="value">
<span v-if="detailData.maxAmount">¥{{ (detailData.maxAmount / 100).toFixed(2) }}</span>
<span v-else class="secondary">无限制</span>
</td>
</tr>
<!-- 最大使用次数 -->
<tr class="alternate">
<td class="label">最大使用次数</td>
<td class="value">
<span v-if="detailData.maxTimes">{{ detailData.maxTimes }}</span>
<span v-else class="secondary">无限制</span>
</td>
</tr>
<!-- 单用户次数 -->
<tr>
<td class="label">单用户次数</td>
<td class="value">
<span v-if="detailData.userTimes">{{ detailData.userTimes }}</span>
<span v-else class="secondary">无限制</span>
</td>
</tr>
<!-- 有效期仅代金券 -->
<tr v-if="type === 'coupon'" class="alternate">
<td class="label">有效期()</td>
<td class="value">
{{ detailData.duration ? (detailData.duration / 86400).toFixed(0) + '天' : '-' }}
</td>
</tr>
<!-- 有效期开始 -->
<tr :class="type === 'coupon' ? '' : 'alternate'">
<td class="label">{{ type === 'code' ? '有效期开始' : '发放时间开始' }}</td>
<td class="value">{{ formatISODate(detailData.startTime) }}</td>
</tr>
<!-- 有效期结束 -->
<tr :class="type === 'coupon' ? 'alternate' : ''">
<td class="label">{{ type === 'code' ? '有效期结束' : '发放时间结束' }}</td>
<td class="value">{{ formatISODate(detailData.endTime) }}</td>
</tr>
<!-- 续费可用 -->
<tr :class="type === 'coupon' ? '' : 'alternate'">
<td class="label">续费可用</td>
<td class="value">
<span :class="['status-icon', detailData.renew ? 'success' : 'danger']">
{{ detailData.renew ? '✓ 是' : '✗ 否' }}
</span>
</td>
</tr>
<!-- 同类型可叠加 -->
<tr :class="type === 'coupon' ? 'alternate' : ''">
<td class="label">同类型可叠加</td>
<td class="value">
<span :class="['status-icon', detailData.canStacking ? 'success' : 'danger']">
{{ detailData.canStacking ? '✓ 是' : '✗ 否' }}
</span>
</td>
</tr>
<!-- 其他类型可叠加 -->
<tr :class="type === 'coupon' ? '' : 'alternate'">
<td class="label">其他类型可叠加</td>
<td class="value">
<span :class="['status-icon', detailData.canCombine ? 'success' : 'danger']">
{{ detailData.canCombine ? '✓ 是' : '✗ 否' }}
</span>
</td>
</tr>
<!-- 创建时间 -->
<tr :class="type === 'coupon' ? 'alternate' : ''">
<td class="label">创建时间</td>
<td class="value timestamp">{{ formatISODate(detailData.CreatedAt) }}</td>
</tr>
<!-- 更新时间 -->
<tr>
<td class="label">更新时间</td>
<td class="value timestamp">{{ formatISODate(detailData.UpdatedAt) }}</td>
</tr>
</table>
</div>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
type: {
type: String,
required: true,
validator: (value) => ['code', 'coupon'].includes(value)
},
detailData: {
type: Object,
default: null
}
})
const emit = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const dialogTitle = computed(() => {
return props.type === 'code' ? '优惠码详情' : '代金券详情'
})
// 格式化ISO 8601日期字符串
const formatISODate = (isoStr) => {
if (!isoStr) return '-'
try {
const date = new Date(isoStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} catch {
return isoStr
}
}
const handleClose = () => {
emit('update:modelValue', false)
}
</script>
<style scoped>
.detail-content {
max-height: 500px;
overflow-y: auto;
padding: 10px;
}
.detail-table {
width: 100%;
border-collapse: collapse;
}
.detail-table tr {
border-bottom: 1px solid #f0f0f0;
}
.detail-table tr.alternate {
background-color: #fafafa;
}
.detail-table td {
padding: 12px 8px;
}
.detail-table .label {
width: 140px;
color: #606266;
font-weight: 500;
}
.detail-table .value {
color: #303133;
}
.detail-table .value.secondary {
color: #606266;
}
.detail-table .value.timestamp {
color: #909399;
font-size: 13px;
}
/* 类型标签 */
.type-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
display: inline-block;
}
.type-tag.percentage {
background-color: #f0f9ff;
color: #67c23a;
}
.type-tag.amount {
background-color: #eff6ff;
color: #409eff;
}
/* 突出显示的值 */
.highlight-value {
font-weight: bold;
font-size: 18px;
}
.highlight-value.percentage {
color: #67c23a;
}
.highlight-value.amount {
color: #f56c6c;
}
/* 状态图标 */
.status-icon.success {
color: #67c23a;
}
.status-icon.danger {
color: #f56c6c;
}
.secondary {
color: #909399;
}
</style>