新加入图片资源管理功能,已更新到线上地址,并开通了批量上传图片权限

This commit is contained in:
ktianc 2023-08-25 15:09:11 +08:00
parent 34248f3c95
commit ee4f4612fe
18 changed files with 685 additions and 9 deletions

View File

@ -0,0 +1,185 @@
<script setup lang="ts">
import { Form } from '@/components/Form'
import { useForm } from '@/hooks/web/useForm'
import { PropType, reactive, watch, ref } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { schema } from './images.data'
import { ElUpload } from 'element-plus'
import { ElMessage, ElImageViewer } from 'element-plus'
import type {
UploadProps,
UploadUserFile,
UploadRequestOptions,
UploadFile,
UploadFiles
} from 'element-plus'
const { required } = useValidator()
const dialogVisible = ref(false)
const imgIndex = ref(0)
const maxLimit = ref(50)
const props = defineProps({
currentRow: {
type: Object as PropType<Nullable<any>>,
default: () => null
}
})
const rules = reactive({
name: [required()]
})
const { register, methods, elFormRef } = useForm({
schema: schema
})
watch(
() => props.currentRow,
(currentRow) => {
if (!currentRow) return
const { setValues } = methods
setValues(currentRow)
},
{
deep: true,
immediate: true
}
)
const { setValue } = methods
const fileList = ref<UploadUserFile[]>([])
const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile) => {
const isIMAGE = ['image/jpeg', 'image/gif', 'image/png'].includes(rawFile.type)
const isLtSize = rawFile.size / 1024 / 1024 < 3
if (!isIMAGE) {
ElMessage.error('上传图片素材必须是 JPG/PNG/ 格式!')
}
if (!isLtSize) {
ElMessage.error('上传图片素材大小不能超过 3MB!')
}
return isIMAGE && isLtSize
}
//
const handleUploadSuccess: UploadProps['onSuccess'] = (
response: any,
uploadFile: UploadFile,
uploadFiles: UploadFiles
) => {
fileList.value = uploadFiles
setValue('images', uploadFiles)
}
//
const handleHttpRequest: UploadProps['httpRequest'] = (options: UploadRequestOptions) => {
return new Promise((resolve) => {
resolve(options)
})
}
const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile: UploadFile) => {
imgIndex.value = fileList.value.findIndex((item) => item.uid === uploadFile.uid)
dialogVisible.value = true
}
const handleCloseViewer = () => {
dialogVisible.value = false
}
const handleExceedLimit = () => {
ElMessage.error('上传失败,超出图片最大数量限制!')
}
defineExpose({
elFormRef,
getFormData: methods.getFormData
})
</script>
<template>
<Form :rules="rules" label-position="top" @register="register">
<template #images>
<div class="flex justify-between w-[100%]">
<span>图片资源</span>
<span>最大数量限制{{ fileList.length }}/{{ maxLimit }}</span>
</div>
<ElUpload
class="resource-image-uploader"
action="#"
:http-request="handleHttpRequest"
v-model:file-list="fileList"
:show-file-list="true"
:multiple="true"
:before-upload="beforeImageUpload"
:on-success="handleUploadSuccess"
:on-preview="handlePictureCardPreview"
:on-exceed="handleExceedLimit"
accept="image/jpeg,image/png"
name="file"
list-type="picture-card"
:limit="maxLimit"
:drag="true"
:disabled="maxLimit <= fileList.length"
>
<div v-if="fileList.length < maxLimit">
<div class="resource-image-uploader-icon">
<Icon icon="akar-icons:plus" :size="23" />
<span class="text-[12px] mt-4">点击或拖拽到这里</span>
<span class="text-[12px] mt-4">{{ fileList.length }}/{{ maxLimit }}</span>
</div>
</div>
<div v-else>
<div class="resource-image-uploader-icon">
<span class="text-[12px]">已到最大限制</span>
<span class="text-[12px] mt-4">{{ fileList.length }}/{{ maxLimit }}</span>
</div>
</div>
</ElUpload>
</template>
</Form>
<ElImageViewer
v-if="dialogVisible"
:z-index="9999"
@close="handleCloseViewer"
:url-list="fileList.map((item) => item.url as string)"
:initial-index="imgIndex"
/>
</template>
<style lang="less">
.resource-image-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
.el-upload-dragger {
padding: 0;
}
.resource-image-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 148px;
height: 148px;
text-align: center;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
}
}
.resource-image-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
</style>

View File

@ -0,0 +1,103 @@
import { FormSchema } from '@/types/form'
import { TableColumn } from '@/types/table'
import { reactive } from 'vue'
export const columns = reactive<TableColumn[]>([
{
field: 'id',
label: '编号',
show: true,
disabled: false,
width: '120px',
span: 24
},
{
field: 'image_url',
label: '图片',
show: true,
disabled: true,
minWidth: '90px',
span: 24
},
{
field: 'remark',
label: '备注',
show: false,
disabled: false,
span: 24
},
{
field: 'update_datetime',
label: '更新时间',
show: false,
width: '180px',
span: 24
},
{
field: 'create_datetime',
label: '创建时间',
width: '180px',
show: true,
span: 24
},
{
field: 'create_user.name',
label: '创建人',
show: false,
span: 24
},
{
field: 'action',
label: '操作',
show: true,
disabled: false,
width: '260px',
fixed: 'right',
span: 24
}
])
export const searchSchema = reactive<FormSchema[]>([
{
field: 'filename',
label: '文件名称',
component: 'Input',
componentProps: {
clearable: true,
style: {
width: '214px'
}
}
}
])
export const schema = reactive<FormSchema[]>([
{
field: 'upload_method',
label: '上传方式',
colProps: {
span: 24
},
component: 'Radio',
componentProps: {
options: [
{
label: '同时上传',
value: '1'
},
{
label: '按顺序上传',
value: '2'
}
]
},
value: '1'
},
{
field: 'images',
label: '',
colProps: {
span: 24
}
}
])

View File

@ -0,0 +1,218 @@
<script lang="ts">
export default {
name: 'ResourceImages'
}
</script>
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { Table } from '@/components/Table'
import { addImagesApi, getImagesListApi, delImagesListApi } from '@/api/vadmin/resource/images'
import { useTable } from '@/hooks/web/useTable'
import { columns, searchSchema } from './components/images.data'
import { ref, watch, nextTick, unref } from 'vue'
import { ElRow, ElButton, ElCol, ElImage, ElMessage } from 'element-plus'
import { RightToolbar } from '@/components/RightToolbar'
import { useCache } from '@/hooks/web/useCache'
import { useRouter } from 'vue-router'
import Write from './components/Write.vue'
import { Dialog } from '@/components/Dialog'
import { useI18n } from '@/hooks/web/useI18n'
import { Search } from '@/components/Search'
import { useClipboard } from '@vueuse/core'
const { wsCache } = useCache()
const { t } = useI18n()
const { register, elTableRef, tableObject, methods } = useTable({
getListApi: getImagesListApi,
delListApi: delImagesListApi,
response: {
data: 'data',
count: 'count'
}
})
const { getList, setSearchParams } = methods
const tableSize = ref('default')
watch(tableSize, (val) => {
tableSize.value = val
})
const route = useRouter()
const cacheTableHeadersKey = route.currentRoute.value.fullPath
watch(
columns,
async (val) => {
wsCache.set(cacheTableHeadersKey, JSON.stringify(val))
await nextTick()
elTableRef.value?.doLayout()
},
{
deep: true
}
)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const loading = ref(false)
const actionType = ref('')
//
const addAction = async () => {
dialogTitle.value = '新增图片素材'
tableObject.currentRow = null
dialogVisible.value = true
actionType.value = 'add'
}
//
const delAction = async (row: any) => {
const { delListApi } = methods
if (row) {
await delListApi(true, [row.id])
} else {
await delListApi(true)
}
}
const writeRef = ref<ComponentRef<typeof Write>>()
//
const save = async () => {
const write = unref(writeRef)
await write?.elFormRef?.validate(async (isValid) => {
if (isValid) {
loading.value = true
let data = await write?.getFormData()
data?.images.forEach((item) => (item.status = 'uploading'))
if (data?.upload_method === '2') {
//
for (const item of data?.images) {
const formData = new FormData()
formData.append('file', item.raw)
await addImagesApi(formData)
item.status = 'success'
}
} else if (data?.upload_method === '1') {
//
const uploadPromises = data?.images.map(async (item) => {
const formData = new FormData()
formData.append('file', item.raw)
await addImagesApi(formData)
item.status = 'success'
})
await Promise.all(uploadPromises)
}
//
getList()
dialogVisible.value = false
loading.value = false
}
})
}
//
const toCopy = async (value: string) => {
// 线线使 https 使
const { copy } = useClipboard()
await copy(value)
return ElMessage.success('复制成功')
}
getList()
</script>
<template>
<ContentWrap>
<Search :schema="searchSchema" @search="setSearchParams" @reset="setSearchParams" />
<div class="mb-8px flex justify-between">
<ElRow :gutter="10">
<ElCol :span="1.5" v-shop>
<ElButton type="primary" @click="addAction">新增图片素材</ElButton>
</ElCol>
<ElCol :span="1.5" v-shop>
<ElButton type="danger" @click="delAction(null)">批量删除选中的图片素材</ElButton>
</ElCol>
</ElRow>
<RightToolbar
@get-list="getList"
v-model:table-size="tableSize"
v-model:columns="columns"
:cache-table-headers-key="cacheTableHeadersKey"
/>
</div>
<Table
v-model:limit="tableObject.limit"
v-model:page="tableObject.page"
:columns="columns"
:data="tableObject.tableData"
:loading="tableObject.loading"
:selection="true"
:size="tableSize"
:border="true"
:pagination="{
total: tableObject.count
}"
@register="register"
>
<template #image_url="{ row, $index }">
<div class="resource-image-name flex items-center">
<div>
<ElImage
:src="`${row.image_url}?x-oss-process=image/resize,m_fixed,h_100`"
:zoom-rate="1.2"
:preview-src-list="tableObject.tableData.map((item) => item.image_url)"
:preview-teleported="true"
:initial-index="$index"
style="height: 60px; display: block"
fit="cover"
/>
</div>
<div class="leading-[35px] ml-2 truncate">
<span>{{ row.filename }}</span>
</div>
</div>
</template>
<template #action="{ row }">
<ElButton type="primary" link size="small" @click="toCopy(row.id)"> 复制图片编号 </ElButton>
<ElButton type="primary" link size="small" @click="toCopy(row.image_url)">
复制图片链接
</ElButton>
<ElButton v-shop type="danger" link size="small" @click="delAction(row)">
{{ t('exampleDemo.del') }}
</ElButton>
</template>
</Table>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="996px" height="600px" top="3vh">
<Write ref="writeRef" :current-row="tableObject.currentRow" />
<template #footer>
<ElButton type="primary" :loading="loading" @click="save">
{{ t('exampleDemo.save') }}
</ElButton>
<ElButton @click="dialogVisible = false">{{ t('dialogDemo.close') }}</ElButton>
</template>
</Dialog>
</ContentWrap>
</template>
<style lang="less">
.resource-image .image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 60px;
height: 35px;
background: var(--el-fill-color-light);
color: var(--el-text-color-secondary);
font-size: 14px;
}
</style>

View File

@ -253,7 +253,7 @@ def __dict_filter(self, **kwargs) -> list[BinaryExpression]:
查询所有用户id为1或2或 4或6并且email不为空并且名称包括李 查询所有用户id为1或2或 4或6并且email不为空并且名称包括李
```python ```python
users = UserDal(db).get_datas(limit=0, id=("in", [1,2,4,6]), email=("not None"), name=("like", "李")) users = UserDal(db).get_datas(limit=0, id=("in", [1,2,4,6]), email=("not None", ), name=("like", "李"))
# limit=0表示返回所有结果数据 # limit=0表示返回所有结果数据
# 这里的 get_datas 默认返回的是 pydantic 模型数据 # 这里的 get_datas 默认返回的是 pydantic 模型数据
@ -261,7 +261,7 @@ users = UserDal(db).get_datas(limit=0, id=("in", [1,2,4,6]), email=("not None"),
users = UserDal(db).get_datas( users = UserDal(db).get_datas(
limit=0, limit=0,
id=("in", [1,2,4,6]), id=("in", [1,2,4,6]),
email=("not None"), email=("not None", ),
name=("like", "李"), name=("like", "李"),
v_return_objs=True v_return_objs=True
) )

View File

@ -11,10 +11,10 @@ from fastapi.security import OAuth2PasswordBearer
""" """
系统版本 系统版本
""" """
VERSION = "2.0.0" VERSION = "2.1.0"
"""安全警告: 不要在生产中打开调试运行!""" """安全警告: 不要在生产中打开调试运行!"""
DEBUG = True DEBUG = False
"""是否开启演示功能取消所有POST,DELETE,PUT操作权限""" """是否开启演示功能取消所有POST,DELETE,PUT操作权限"""
DEMO = not DEBUG DEMO = not DEBUG
@ -24,6 +24,7 @@ DEMO_WHITE_LIST_PATH = [
"/auth/token/refresh", "/auth/token/refresh",
"/auth/wx/login", "/auth/wx/login",
"/vadmin/system/dict/types/details", "/vadmin/system/dict/types/details",
"/vadmin/resource/images",
"/vadmin/auth/user/export/query/list/to/excel" "/vadmin/auth/user/export/query/list/to/excel"
] ]

View File

@ -12,6 +12,7 @@ from apps.vadmin.record.views import app as vadmin_record_app
from apps.vadmin.workplace.views import app as vadmin_workplace_app from apps.vadmin.workplace.views import app as vadmin_workplace_app
from apps.vadmin.analysis.views import app as vadmin_analysis_app from apps.vadmin.analysis.views import app as vadmin_analysis_app
from apps.vadmin.help.views import app as vadmin_help_app from apps.vadmin.help.views import app as vadmin_help_app
from apps.vadmin.resource.views import app as vadmin_resource_app
# 引入应用中的路由 # 引入应用中的路由
@ -23,4 +24,5 @@ urlpatterns = [
{"ApiRouter": vadmin_workplace_app, "prefix": "/vadmin/workplace", "tags": ["工作区管理"]}, {"ApiRouter": vadmin_workplace_app, "prefix": "/vadmin/workplace", "tags": ["工作区管理"]},
{"ApiRouter": vadmin_analysis_app, "prefix": "/vadmin/analysis", "tags": ["数据分析管理"]}, {"ApiRouter": vadmin_analysis_app, "prefix": "/vadmin/analysis", "tags": ["数据分析管理"]},
{"ApiRouter": vadmin_help_app, "prefix": "/vadmin/help", "tags": ["帮助中心管理"]}, {"ApiRouter": vadmin_help_app, "prefix": "/vadmin/help", "tags": ["帮助中心管理"]},
{"ApiRouter": vadmin_resource_app, "prefix": "/vadmin/resource", "tags": ["资源管理"]},
] ]

View File

@ -9,7 +9,7 @@
from sqlalchemy.orm import relationship, Mapped, mapped_column from sqlalchemy.orm import relationship, Mapped, mapped_column
from apps.vadmin.auth.models import VadminUser from apps.vadmin.auth.models import VadminUser
from db.db_base import BaseModel from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Integer, ForeignKey from sqlalchemy import String, Boolean, Integer, ForeignKey, Text
class VadminIssueCategory(BaseModel): class VadminIssueCategory(BaseModel):
@ -42,7 +42,7 @@ class VadminIssue(BaseModel):
category: Mapped[list["VadminIssueCategory"]] = relationship(foreign_keys=category_id, back_populates='issues') category: Mapped[list["VadminIssueCategory"]] = relationship(foreign_keys=category_id, back_populates='issues')
title: Mapped[str] = mapped_column(String(255), index=True, nullable=False, comment="标题") title: Mapped[str] = mapped_column(String(255), index=True, nullable=False, comment="标题")
content: Mapped[str] = mapped_column(String(5000), comment="内容") content: Mapped[str] = mapped_column(Text, comment="内容")
view_number: Mapped[int] = mapped_column(Integer, default=0, comment="查看次数") view_number: Mapped[int] = mapped_column(Integer, default=0, comment="查看次数")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可见") is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可见")

View File

@ -14,7 +14,7 @@ from apps.vadmin.auth.utils.validation import LoginForm, WXLoginForm
from utils.ip_manage import IPManage from utils.ip_manage import IPManage
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from db.db_base import BaseModel from db.db_base import BaseModel
from sqlalchemy import String, Boolean from sqlalchemy import String, Boolean, Text
from fastapi import Request from fastapi import Request
from starlette.requests import Request as StarletteRequest from starlette.requests import Request as StarletteRequest
from user_agents import parse from user_agents import parse
@ -39,8 +39,8 @@ class VadminLoginRecord(BaseModel):
area_code: Mapped[str | None] = mapped_column(String(255), comment="地区区号") area_code: Mapped[str | None] = mapped_column(String(255), comment="地区区号")
browser: Mapped[str | None] = mapped_column(String(50), comment="浏览器") browser: Mapped[str | None] = mapped_column(String(50), comment="浏览器")
system: Mapped[str | None] = mapped_column(String(50), comment="操作系统") system: Mapped[str | None] = mapped_column(String(50), comment="操作系统")
response: Mapped[str | None] = mapped_column(String(5000), comment="响应信息") response: Mapped[str | None] = mapped_column(Text, comment="响应信息")
request: Mapped[str | None] = mapped_column(String(5000), comment="请求信息") request: Mapped[str | None] = mapped_column(Text, comment="请求信息")
@classmethod @classmethod
async def create_login_record( async def create_login_record(

View File

@ -0,0 +1,17 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 13:15
# @File : crud.py
# @IDE : PyCharm
# @desc : 简要说明
from sqlalchemy.ext.asyncio import AsyncSession
from core.crud import DalBase
from . import models, schemas
class ImagesDal(DalBase):
def __init__(self, db: AsyncSession):
super(ImagesDal, self).__init__(db, models.VadminImages, schemas.ImagesSimpleOut)

View File

@ -0,0 +1 @@
from .images import VadminImages

View File

@ -0,0 +1,27 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 13:41
# @File : images.py
# @IDE : PyCharm
# @desc : 图片素材表
from sqlalchemy.orm import relationship, Mapped, mapped_column
from apps.vadmin.auth.models import VadminUser
from db.db_base import BaseModel
from sqlalchemy import String, ForeignKey, Integer
class VadminImages(BaseModel):
__tablename__ = "vadmin_resource_images"
__table_args__ = ({'comment': '图片素材表'})
filename: Mapped[str] = mapped_column(String(255), nullable=False, comment="原图片名称")
image_url: Mapped[str] = mapped_column(String(500), nullable=False, comment="图片链接")
create_user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_auth_user.id", ondelete='RESTRICT'),
comment="创建人"
)
create_user: Mapped[VadminUser] = relationship(foreign_keys=create_user_id)

View File

@ -0,0 +1 @@
from .images import ImagesParams

View File

@ -0,0 +1,27 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 14:59
# @File : images.py
# @IDE : PyCharm
# @desc : 简要说明
from fastapi import Depends
from core.dependencies import Paging, QueryParams
class ImagesParams(QueryParams):
"""
列表分页
"""
def __init__(
self,
filename: str = None,
params: Paging = Depends()
):
super().__init__(params)
self.filename = ('like', filename)
self.v_order = "desc"
self.v_order_field = "create_datetime"

View File

@ -0,0 +1 @@
from .images import Images, ImagesOut, ImagesSimpleOut

View File

@ -0,0 +1,33 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 14:49
# @File : images.py
# @IDE : PyCharm
# @desc : 简要说明
from pydantic import BaseModel, ConfigDict
from core.data_types import DatetimeStr
from apps.vadmin.auth.schemas import UserSimpleOut
class Images(BaseModel):
filename: str
image_url: str
create_user_id: int
class ImagesSimpleOut(Images):
model_config = ConfigDict(from_attributes=True)
id: int
create_datetime: DatetimeStr
update_datetime: DatetimeStr
class ImagesOut(ImagesSimpleOut):
model_config = ConfigDict(from_attributes=True)
create_user: UserSimpleOut

View File

@ -0,0 +1,60 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/8/25 9:29
# @File : views.py
# @IDE : PyCharm
# @desc : 简要说明
from fastapi import APIRouter, Depends, UploadFile
from sqlalchemy.orm import joinedload
from core.dependencies import IdList
from utils.file.aliyun_oss import AliyunOSS, BucketConf
from utils.response import SuccessResponse
from . import schemas, crud, params, models
from apps.vadmin.auth.utils.current import FullAdminAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from application.settings import ALIYUN_OSS
app = APIRouter()
###########################################################
# 图片资源管理
###########################################################
@app.get("/images", summary="获取图片列表")
async def get_images_list(p: params.ImagesParams = Depends(), auth: Auth = Depends(FullAdminAuth())):
model = models.VadminImages
v_options = [joinedload(model.create_user)]
v_schema = schemas.ImagesOut
datas, count = await crud.ImagesDal(auth.db).get_datas(
**p.dict(),
v_options=v_options,
v_schema=v_schema,
v_return_count=True
)
return SuccessResponse(datas, count=count)
@app.post("/images", summary="创建图片")
async def create_images(file: UploadFile, auth: Auth = Depends(FullAdminAuth())):
filepath = f"/resource/images/"
result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_image(filepath, file)
data = schemas.Images(
filename=file.filename,
image_url=result,
create_user_id=auth.user.id
)
return SuccessResponse(await crud.ImagesDal(auth.db).create_data(data=data))
@app.delete("/images", summary="删除图片", description="硬删除")
async def delete_images(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth())):
await crud.ImagesDal(auth.db).delete_datas(ids.ids, v_soft=False)
return SuccessResponse("删除成功")
@app.get("/images/{data_id}", summary="获取图片信息")
async def get_images(data_id: int, auth: Auth = Depends(FullAdminAuth())):
return SuccessResponse(await crud.ImagesDal(auth.db).get_data(data_id, v_schema=schemas.ImagesSimpleOut))