|
|
@@ -1,6 +1,8 @@
|
|
|
<script setup lang="ts">
|
|
|
import { MaterialAPI } from '@/config/API.ts'
|
|
|
import { useFileUpload } from '@/hooks/File/useFileUpload.ts'
|
|
|
+import { useMaterial } from '@/hooks/Material/useMaterial.ts'
|
|
|
+import router from '@/router'
|
|
|
import type { ResponseInfo } from '@/types/axios.ts'
|
|
|
|
|
|
import axiosInstance from '@/utils/axios/axiosInstance.ts'
|
|
|
@@ -8,26 +10,29 @@ import type {
|
|
|
AddTagRes,
|
|
|
GameData,
|
|
|
GameSelectItem,
|
|
|
+ ImgUploadRes,
|
|
|
ResFileInfo,
|
|
|
TagItem,
|
|
|
UploadAssetForm,
|
|
|
} from '@/views/Material/types/uploadFileType.ts'
|
|
|
-import { Plus } from '@element-plus/icons-vue'
|
|
|
import type {
|
|
|
FormInstance,
|
|
|
FormRules,
|
|
|
UploadFile,
|
|
|
UploadFiles,
|
|
|
UploadInstance,
|
|
|
- UploadRawFile,
|
|
|
UploadRequestOptions,
|
|
|
- UploadUserFile,
|
|
|
} from 'element-plus'
|
|
|
-import { ElMessageBox, genFileId } from 'element-plus'
|
|
|
-import { nextTick, onMounted, reactive, ref, toRaw } from 'vue'
|
|
|
+import { ElMessageBox } from 'element-plus'
|
|
|
+import { nextTick, onMounted, type Reactive, reactive, ref, toRaw } from 'vue'
|
|
|
+import { VideoProcessor } from '@/utils/file/handleVideoFile.ts'
|
|
|
+import { ImageProcessor } from '@/utils/file/handleImgFile.ts'
|
|
|
|
|
|
-const { isValidFile, handleFile, getAllTags } = useFileUpload()
|
|
|
+const { getAllTags } = useMaterial()
|
|
|
+const { isValidFile, handleFile } = useFileUpload()
|
|
|
+const { getGameInfo, createFilter } = useMaterial()
|
|
|
|
|
|
+// 表单数据
|
|
|
const uploadForm = reactive<UploadAssetForm>({
|
|
|
name: '',
|
|
|
tags: [],
|
|
|
@@ -35,8 +40,14 @@ const uploadForm = reactive<UploadAssetForm>({
|
|
|
pid: '',
|
|
|
filePath: '',
|
|
|
md5: '',
|
|
|
+ isImg9: 1,
|
|
|
+ resolution: {
|
|
|
+ width: 0,
|
|
|
+ height: 0,
|
|
|
+ },
|
|
|
})
|
|
|
|
|
|
+// 表单规则
|
|
|
const rules = reactive<FormRules<UploadAssetForm>>({
|
|
|
name: [
|
|
|
{ required: true, message: '请输入创意名', trigger: 'blur' },
|
|
|
@@ -46,31 +57,52 @@ const rules = reactive<FormRules<UploadAssetForm>>({
|
|
|
{ required: true, message: '请选择标签', trigger: 'change' },
|
|
|
{ required: true, message: '请选择标签', trigger: 'blur' },
|
|
|
],
|
|
|
+ isImg9: [
|
|
|
+ { required: true, message: '请选择是否是九宫格', trigger: 'change' },
|
|
|
+ ],
|
|
|
gid: [{ required: true, message: '请选择游戏', trigger: 'change' }],
|
|
|
filePath: [{ required: true, message: '请选择素材', trigger: 'change' }],
|
|
|
})
|
|
|
|
|
|
-let gameInfoList: GameData = {}
|
|
|
-const gameSelect = reactive<GameSelectItem[]>([])
|
|
|
-
|
|
|
+const gameSelect = reactive<GameSelectItem[]>([]) // 游戏级联选择框数据
|
|
|
const uploadRef = ref<UploadInstance>()
|
|
|
const fileFormRef = ref<FormInstance>()
|
|
|
-const tagInput = ref('')
|
|
|
+const tagInput = ref('') // 标签输入框数据
|
|
|
const autocompleteRef = ref()
|
|
|
-const isVideo = ref(false)
|
|
|
-const limit = 1
|
|
|
-const allTags: Array<TagItem> = []
|
|
|
+const isVideo = ref(false) // 当前上传的时候是视频
|
|
|
+const limit = 1 // 限制上传文件数量
|
|
|
+const allTags: Array<TagItem> = [] // 所有标签
|
|
|
+const isUploading = ref(false)
|
|
|
+const videoDialogVisible = ref(false)
|
|
|
+const previewUrl = ref('')
|
|
|
+const previewVideoRef = ref<HTMLVideoElement>()
|
|
|
|
|
|
+const coverUrl = ref('') // 封面图片地址
|
|
|
+
|
|
|
+// 级联选择框属性配置
|
|
|
const cascadePropsConfig = {
|
|
|
expandTrigger: 'hover' as const,
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 选择游戏之后的处理
|
|
|
+ * @param value 目前选择的值
|
|
|
+ */
|
|
|
const gameSelectChange = (value: string[]) => {
|
|
|
uploadForm.pid = value[0]
|
|
|
uploadForm.gid = value[1]
|
|
|
}
|
|
|
|
|
|
-const generateGameSelect = () => {
|
|
|
+/**
|
|
|
+ * 生成游戏级联选择框数据
|
|
|
+ *
|
|
|
+ * @param gameInfoList 游戏信息
|
|
|
+ * @param gameSelect 级联选择框的值
|
|
|
+ */
|
|
|
+const generateGameSelect = (
|
|
|
+ gameInfoList: GameData,
|
|
|
+ gameSelect: Reactive<GameSelectItem[]>,
|
|
|
+) => {
|
|
|
gameSelect.splice(0, gameSelect.length)
|
|
|
for (let [k, v] of Object.entries(gameInfoList)) {
|
|
|
const children = v.map(item => {
|
|
|
@@ -87,6 +119,11 @@ const generateGameSelect = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 标签搜索
|
|
|
+ * @param queryString 搜索值
|
|
|
+ * @param cb 回调
|
|
|
+ */
|
|
|
const tagSearch = async (queryString: string, cb: any) => {
|
|
|
const result = queryString
|
|
|
? allTags.filter(createFilter(queryString))
|
|
|
@@ -95,101 +132,120 @@ const tagSearch = async (queryString: string, cb: any) => {
|
|
|
cb(result)
|
|
|
}
|
|
|
|
|
|
-const createFilter = (queryString: string) => {
|
|
|
- return (restaurant: TagItem) => {
|
|
|
- return (
|
|
|
- restaurant.name.toLowerCase().indexOf(queryString.toLowerCase()) === 0
|
|
|
- )
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
+/**
|
|
|
+ * 处理标签选择后的事件
|
|
|
+ * @param tags 选中的标签
|
|
|
+ */
|
|
|
const handleTagSelect = (tags: TagItem) => {
|
|
|
uploadForm.tags.push(tags.id)
|
|
|
+ // 需要在下一帧出发blur,否则无法选中
|
|
|
nextTick(() => {
|
|
|
autocompleteRef.value.blur()
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-const handleExceed = async (
|
|
|
- files: UploadRawFile[],
|
|
|
- fileList: UploadUserFile[],
|
|
|
-) => {
|
|
|
- const confirm = await ElMessageBox.confirm(
|
|
|
- '上传将覆盖前一个文件,是否继续上传?',
|
|
|
- {
|
|
|
- confirmButtonText: '继续上传',
|
|
|
- cancelButtonText: '取消',
|
|
|
- type: 'warning',
|
|
|
- },
|
|
|
- )
|
|
|
- const file = files[0]
|
|
|
- if (!confirm) return false
|
|
|
- handleFile(file, fileList, isVideo, uploadForm)
|
|
|
- nextTick(() => {
|
|
|
- fileFormRef.value?.validateField('filePath')
|
|
|
- })
|
|
|
- uploadRef.value!.clearFiles()
|
|
|
- file.uid = genFileId()
|
|
|
- uploadRef.value!.handleStart(file)
|
|
|
-}
|
|
|
-
|
|
|
+/**
|
|
|
+ * 当文件列表发生变化时的处理事件
|
|
|
+ *
|
|
|
+ * @param file 选择的文件
|
|
|
+ * @param fileList 文件列表
|
|
|
+ */
|
|
|
const handleFileChange = (file: UploadFile, fileList: UploadFiles) => {
|
|
|
if (!file || !file.raw) {
|
|
|
ElMessage.warning('请传入合法文件')
|
|
|
return
|
|
|
}
|
|
|
+ // 不是合法文件直接返回不处理
|
|
|
if (!isValidFile(file)) {
|
|
|
fileList.splice(1)
|
|
|
ElMessage.warning('上传文件超出限制或不是规定文件')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 统一处理文件上传
|
|
|
+ const processUpload = async () => {
|
|
|
+ handleFile(file, fileList, isVideo, uploadForm, previewUrl)
|
|
|
+ nextTick(() => {
|
|
|
+ fileFormRef.value?.validateField('filePath')
|
|
|
+ })
|
|
|
+ // 重置文件状态保证可重复上传
|
|
|
+ fileList.forEach(f => (f.status = 'ready'))
|
|
|
+
|
|
|
+ console.log(file)
|
|
|
+ // 如果是视频的话,还需要生成封面
|
|
|
+ if (isVideo.value) {
|
|
|
+ nextTick(async () => {
|
|
|
+ const videoProcessor = new VideoProcessor(previewUrl.value)
|
|
|
+ const { width, height } = await videoProcessor.getVideoInfo()
|
|
|
+ coverUrl.value = await videoProcessor.generateCover()
|
|
|
+ uploadForm.resolution = {
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ const imgProcessor = new ImageProcessor(file.raw!)
|
|
|
+ const { width, height } = await imgProcessor.getResolution()
|
|
|
+ uploadForm.resolution = {
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ }
|
|
|
+ coverUrl.value = file.url!
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- handleFile(file, fileList, isVideo, uploadForm)
|
|
|
- nextTick(() => {
|
|
|
- fileFormRef.value?.validateField('filePath')
|
|
|
- })
|
|
|
- // 在第一次上传成功后,文件状态会被设置为success,此时是无法重新提交文件上传的,故需要手动重置
|
|
|
- fileList.forEach(file => {
|
|
|
- file.status = 'ready'
|
|
|
- })
|
|
|
-
|
|
|
- console.log(uploadForm)
|
|
|
+ if (fileList.length > limit) {
|
|
|
+ // 超限需要用户确认
|
|
|
+ ElMessageBox.confirm('上传将覆盖前一个文件,是否继续上传?', {
|
|
|
+ confirmButtonText: '继续上传',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning',
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ // 保留最新文件,删除旧文件(假设旧文件在数组开头)
|
|
|
+ fileList.splice(0, 1)
|
|
|
+ processUpload()
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ // 取消时移除当前新增的文件
|
|
|
+ const newFileIndex = fileList.findIndex(f => f.uid === file.uid)
|
|
|
+ if (newFileIndex > -1) fileList.splice(newFileIndex, 1)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // 未超限直接上传
|
|
|
+ processUpload()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 处理表单提交
|
|
|
+ *
|
|
|
+ * 这个函数不处理表单的提交,只是作为触发器,完成表单验证的功能,然后将后续流程交给文件上传部分
|
|
|
+ * @param formEl 表单对象
|
|
|
+ */
|
|
|
const submitAsset = async (formEl: FormInstance | undefined) => {
|
|
|
+ if (isUploading.value) return
|
|
|
if (!formEl) return
|
|
|
+ // 表单验证
|
|
|
const isValid = await formEl.validate()
|
|
|
if (!isValid) {
|
|
|
ElMessage.warning('请填写完整信息')
|
|
|
return
|
|
|
}
|
|
|
+ // 交给文件上传的处理流程
|
|
|
uploadRef.value?.submit()
|
|
|
}
|
|
|
|
|
|
-const submitForm = async () => {
|
|
|
- const url = isVideo.value ? MaterialAPI.videoSubmit : MaterialAPI.imgSubmit
|
|
|
- const nFormData = JSON.parse(JSON.stringify(toRaw(uploadForm)))
|
|
|
- if (isVideo.value) {
|
|
|
- nFormData.videoPath = nFormData.filePath
|
|
|
- } else {
|
|
|
- nFormData.imgPath = nFormData.filePath
|
|
|
- }
|
|
|
- delete nFormData.filePath
|
|
|
-
|
|
|
- // TODO 完善图片和视频的返回值类型
|
|
|
- const result = await axiosInstance.post(url, nFormData)
|
|
|
- if (result.code !== 0) {
|
|
|
- ElMessage.error('上传素材失败')
|
|
|
- return
|
|
|
- }
|
|
|
- ElMessage.success('上传素材成功')
|
|
|
- uploadRef.value?.clearFiles()
|
|
|
- tagInput.value = ''
|
|
|
- fileFormRef.value?.resetFields()
|
|
|
-}
|
|
|
-
|
|
|
+/**
|
|
|
+ * 文件上传
|
|
|
+ *
|
|
|
+ * 表单提交需要先拿到文件上传成功后的回显,所以文件上传成功后才会执行表单的提交
|
|
|
+ * @param options 上传文件信息
|
|
|
+ */
|
|
|
const uploadFile = async (options: UploadRequestOptions) => {
|
|
|
+ isUploading.value = true
|
|
|
if (!options || !options.file) {
|
|
|
ElMessage.warning('请先上传文件')
|
|
|
+ isUploading.value = false
|
|
|
return
|
|
|
}
|
|
|
const file = options.file
|
|
|
@@ -201,20 +257,66 @@ const uploadFile = async (options: UploadRequestOptions) => {
|
|
|
)) as ResFileInfo
|
|
|
if (result.code !== 0) {
|
|
|
ElMessage.error('上传素材失败')
|
|
|
+ isUploading.value = false
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
const { md5, path } = result
|
|
|
uploadForm.md5 = md5
|
|
|
uploadForm.filePath = path
|
|
|
+
|
|
|
+ // 拿到回显后执行表单的提交流程
|
|
|
await submitForm()
|
|
|
}
|
|
|
|
|
|
-const goBack = () => {}
|
|
|
+const submitForm = async () => {
|
|
|
+ const url = isVideo.value ? MaterialAPI.videoSubmit : MaterialAPI.imgSubmit
|
|
|
+ const nFormData = JSON.parse(JSON.stringify(toRaw(uploadForm)))
|
|
|
+
|
|
|
+ // 区分视频和图片,他们需要的字段不同
|
|
|
+ if (isVideo.value) {
|
|
|
+ nFormData.videoPath = nFormData.filePath
|
|
|
+ // 对于视频上传需要删除掉九宫格字段信息
|
|
|
+ delete nFormData.isImg9
|
|
|
+ } else {
|
|
|
+ nFormData.imgPath = nFormData.filePath
|
|
|
+ }
|
|
|
+ // 移除不需要的filePath
|
|
|
+ delete nFormData.filePath
|
|
|
+
|
|
|
+ const res = (await axiosInstance.post(url, nFormData)) as ResponseInfo
|
|
|
+ let result = res as ImgUploadRes
|
|
|
+ if (isVideo.value) {
|
|
|
+ result = res as ImgUploadRes
|
|
|
+ }
|
|
|
+ if (result.code !== 0) {
|
|
|
+ ElMessage.error('上传素材失败')
|
|
|
+ isUploading.value = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ElMessage.success('上传素材成功')
|
|
|
+ isUploading.value = false
|
|
|
+ // 重置表单
|
|
|
+ uploadRef.value?.clearFiles()
|
|
|
+ tagInput.value = ''
|
|
|
+ fileFormRef.value?.resetFields()
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 返回上一页
|
|
|
+ */
|
|
|
+const goBack = () => {
|
|
|
+ router.back()
|
|
|
+}
|
|
|
|
|
|
+/**
|
|
|
+ * 新增tag
|
|
|
+ * @param val 传新增值
|
|
|
+ */
|
|
|
const tagAdd = async (val: string) => {
|
|
|
const isExist = allTags.find(item => item.name === val)
|
|
|
const newTagName = val
|
|
|
+ // 首先要判断是否已经存在这个标签,不存在则需要先走新建流程,所以不要直接添加进去,先移除
|
|
|
uploadForm.tags.splice(uploadForm.tags.length - 1, 1)
|
|
|
let id = -1
|
|
|
if (!isExist) {
|
|
|
@@ -241,23 +343,39 @@ const tagAdd = async (val: string) => {
|
|
|
uploadForm.tags.push(id)
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 清空标签输入框
|
|
|
+ */
|
|
|
const clearTagInput = () => {
|
|
|
uploadForm.tags = []
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 更新tag标签列表
|
|
|
+ */
|
|
|
const updateTags = async () => {
|
|
|
const tags = await getAllTags()
|
|
|
allTags.splice(0, allTags.length, ...tags)
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 更新游戏标签,并且重新生成游戏选择列表
|
|
|
+ */
|
|
|
const updateGameInfo = async () => {
|
|
|
- const res = (await axiosInstance.post(
|
|
|
- MaterialAPI.getAllGamesInfo,
|
|
|
- {},
|
|
|
- )) as ResponseInfo
|
|
|
- console.log(res)
|
|
|
- gameInfoList = res.data as GameData
|
|
|
- generateGameSelect()
|
|
|
+ const info = await getGameInfo()
|
|
|
+ if (info === null) {
|
|
|
+ ElMessage.error('获取游戏信息失败')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ generateGameSelect(info, gameSelect)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 处理预览
|
|
|
+ */
|
|
|
+const handlePreview = () => {
|
|
|
+ videoDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
onMounted(async () => {
|
|
|
@@ -291,6 +409,12 @@ onMounted(async () => {
|
|
|
v-model="uploadForm.name"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
+ <el-form-item v-if="!isVideo" label="是否是九宫格图片" prop="isImg9">
|
|
|
+ <el-radio-group v-model="uploadForm.isImg9">
|
|
|
+ <el-radio :value="1">是</el-radio>
|
|
|
+ <el-radio :value="0">否</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
<el-form-item label="游戏" prop="gid">
|
|
|
<el-cascader
|
|
|
v-model="uploadForm.gid"
|
|
|
@@ -335,6 +459,7 @@ onMounted(async () => {
|
|
|
</el-input-tag>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
+ <!-- :limit="limit"-->
|
|
|
<el-form-item label="文件" prop="filePath">
|
|
|
<div class="fileContainer">
|
|
|
<el-upload
|
|
|
@@ -342,12 +467,27 @@ onMounted(async () => {
|
|
|
drag
|
|
|
:auto-upload="false"
|
|
|
list-type="picture"
|
|
|
- :limit="limit"
|
|
|
:on-change="handleFileChange"
|
|
|
- :on-exceed="handleExceed"
|
|
|
ref="uploadRef"
|
|
|
:http-request="uploadFile"
|
|
|
>
|
|
|
+ <template #file="{ file }">
|
|
|
+ <div class="fileInfo">
|
|
|
+ <div class="fileIcon" @click="handlePreview">
|
|
|
+ <el-image
|
|
|
+ :src="coverUrl"
|
|
|
+ class="fileListPreviewContent fileListPreviewImg"
|
|
|
+ />
|
|
|
+ <div class="fileListPreviewContent fileListPreviewMask">
|
|
|
+ <el-icon>
|
|
|
+ <zoom-in />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div>{{ file.name }}</div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
<el-icon class="el-icon--upload">
|
|
|
<upload-filled />
|
|
|
</el-icon>
|
|
|
@@ -365,12 +505,30 @@ onMounted(async () => {
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
+ <el-dialog
|
|
|
+ v-model="videoDialogVisible"
|
|
|
+ title="预览"
|
|
|
+ class="previewDialog"
|
|
|
+ >
|
|
|
+ <div class="previewContainer">
|
|
|
+ <video
|
|
|
+ class="previewContent"
|
|
|
+ v-if="isVideo"
|
|
|
+ :src="previewUrl"
|
|
|
+ width="400"
|
|
|
+ height="400"
|
|
|
+ controls
|
|
|
+ ref="previewVideoRef"
|
|
|
+ ></video>
|
|
|
+ <img class="previewContent" v-else :src="previewUrl" alt="" />
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
<div class="fileFooter">
|
|
|
<el-button
|
|
|
color="#197AFB"
|
|
|
- :icon="Plus"
|
|
|
- class="el-button-mini"
|
|
|
+ class="el-button-mini w120"
|
|
|
@click="submitAsset(fileFormRef)"
|
|
|
+ :loading="isUploading"
|
|
|
>确定
|
|
|
</el-button>
|
|
|
</div>
|
|
|
@@ -439,7 +597,53 @@ onMounted(async () => {
|
|
|
color: #b6b7b7;
|
|
|
}
|
|
|
|
|
|
-.fileItem {
|
|
|
+.fileInfo {
|
|
|
display: flex;
|
|
|
+ align-items: center;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.fileIcon {
|
|
|
+ margin-right: 10px;
|
|
|
+ position: relative;
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.fileIcon:hover .fileListPreviewMask {
|
|
|
+ visibility: visible;
|
|
|
+}
|
|
|
+
|
|
|
+.fileListPreviewContent {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.fileListPreviewMask {
|
|
|
+ visibility: hidden;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background-color: rgba(0, 0, 0, 0.6);
|
|
|
+ color: white;
|
|
|
+ font-size: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.previewDialog {
|
|
|
+ width: 800px;
|
|
|
+ height: 800px;
|
|
|
+}
|
|
|
+
|
|
|
+.previewContainer {
|
|
|
+ width: 80%;
|
|
|
+ height: 100%;
|
|
|
+ margin: 0 auto;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
</style>
|