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

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不为空并且名称包括李
```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表示返回所有结果数据
# 这里的 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(
limit=0,
id=("in", [1,2,4,6]),
email=("not None"),
email=("not None", ),
name=("like", "李"),
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操作权限"""
DEMO = not DEBUG
@ -24,6 +24,7 @@ DEMO_WHITE_LIST_PATH = [
"/auth/token/refresh",
"/auth/wx/login",
"/vadmin/system/dict/types/details",
"/vadmin/resource/images",
"/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.analysis.views import app as vadmin_analysis_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_analysis_app, "prefix": "/vadmin/analysis", "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 apps.vadmin.auth.models import VadminUser
from db.db_base import BaseModel
from sqlalchemy import String, Boolean, Integer, ForeignKey
from sqlalchemy import String, Boolean, Integer, ForeignKey, Text
class VadminIssueCategory(BaseModel):
@ -42,7 +42,7 @@ class VadminIssue(BaseModel):
category: Mapped[list["VadminIssueCategory"]] = relationship(foreign_keys=category_id, back_populates='issues')
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="查看次数")
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 sqlalchemy.ext.asyncio import AsyncSession
from db.db_base import BaseModel
from sqlalchemy import String, Boolean
from sqlalchemy import String, Boolean, Text
from fastapi import Request
from starlette.requests import Request as StarletteRequest
from user_agents import parse
@ -39,8 +39,8 @@ class VadminLoginRecord(BaseModel):
area_code: Mapped[str | None] = mapped_column(String(255), comment="地区区号")
browser: 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="响应信息")
request: Mapped[str | None] = mapped_column(String(5000), comment="请求信息")
response: Mapped[str | None] = mapped_column(Text, comment="响应信息")
request: Mapped[str | None] = mapped_column(Text, comment="请求信息")
@classmethod
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))