This commit is contained in:
jacky 2024-04-08 21:29:09 +08:00
parent 04873151f8
commit 83758dfee9
36 changed files with 4998 additions and 0 deletions

View File

@ -0,0 +1,222 @@
<template>
<el-drawer
v-model="drawer"
title="媒体库"
size="650px"
>
<warning-bar
title="点击“文件名/备注”可以编辑文件名或者备注内容。"
/>
<div class="gva-btn-list">
<upload-common
:image-common="imageCommon"
class="upload-btn-media-library"
@on-success="open"
/>
<upload-image
:image-url="imageUrl"
:file-size="512"
:max-w-h="1080"
class="upload-btn-media-library"
@on-success="open"
/>
<el-form
ref="searchForm"
:inline="true"
:model="search"
>
<el-form-item label="">
<el-input
v-model="search.keyword"
class="keyword"
placeholder="请输入文件名或备注"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="search"
@click="open"
>查询</el-button>
</el-form-item>
</el-form>
</div>
<div class="media">
<div
v-for="(item,key) in picList"
:key="key"
class="media-box"
>
<div class="header-img-box-list">
<el-image
:key="key"
:src="getUrl(item.url)"
@click="chooseImg(item.url,target,targetKey)"
>
<template #error>
<div class="header-img-box-list">
<el-icon>
<picture />
</el-icon>
</div>
</template>
</el-image>
</div>
<div
class="img-title"
@click="editFileNameFunc(item)"
>{{ item.name }}</div>
</div>
</div>
<el-pagination
:current-page="page"
:page-size="pageSize"
:total="total"
:style="{'justify-content':'center'}"
layout="total, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</el-drawer>
</template>
<script setup>
import { getUrl } from '@/utils/image'
import { ref } from 'vue'
import { getFileList, editFileName } from '@/api/fileUploadAndDownload'
import UploadImage from '@/components/upload/image.vue'
import UploadCommon from '@/components/upload/common.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import WarningBar from '@/components/warningBar/warningBar.vue'
const imageUrl = ref('')
const imageCommon = ref('')
const search = ref({})
const page = ref(1)
const total = ref(0)
const pageSize = ref(20)
//
const handleSizeChange = (val) => {
pageSize.value = val
open()
}
const handleCurrentChange = (val) => {
page.value = val
open()
}
const emit = defineEmits(['enterImg'])
defineProps({
target: {
type: Object,
default: null,
},
targetKey: {
type: String,
default: '',
},
})
const drawer = ref(false)
const picList = ref([])
const chooseImg = (url, target, targetKey) => {
if (target && targetKey) {
target[targetKey] = url
}
emit('enterImg', url)
drawer.value = false
}
const open = async() => {
const res = await getFileList({ page: page.value, pageSize: pageSize.value, ...search.value })
if (res.code === 0) {
picList.value = res.data.list
total.value = res.data.total
page.value = res.data.page
pageSize.value = res.data.pageSize
drawer.value = true
}
}
/**
* 编辑文件名或者备注
* @param row
* @returns {Promise<void>}
*/
const editFileNameFunc = async(row) => {
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '不能为空',
inputValue: row.name
}).then(async({ value }) => {
row.name = value
// console.log(row)
const res = await editFileName(row)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '编辑成功!',
})
open()
}
}).catch(() => {
ElMessage({
type: 'info',
message: '取消修改'
})
})
}
defineExpose({ open })
</script>
<style lang="scss">
.upload-btn-media-library {
margin-left: 20px;
}
.media {
display: flex;
flex-wrap: wrap;
.media-box {
width: 120px;
margin-left: 20px;
.img-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 36px;
text-align: center;
cursor: pointer;
}
.header-img-box-list {
width: 120px;
height: 120px;
border: 1px dashed #ccc;
border-radius: 8px;
text-align: center;
line-height: 120px;
cursor: pointer;
overflow: hidden;
.el-image__inner {
max-width: 120px;
max-height: 120px;
vertical-align: middle;
width: unset;
height: unset;
}
}
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<el-dialog
v-model="dialogVisible"
width="30%"
class="overlay"
:show-close="false"
>
<template #header>
<input
v-model="searchInput"
class="quick-input"
placeholder="请输入你需要快捷到达的功能"
>
</template>
<div
v-for="(option,index) in options"
:key="index"
>
<div
v-if="option.children.length"
class="quick-title"
>{{ option.label }}</div>
<div
v-for="(item,key) in option.children"
:key="index+'-'+key"
class="quick-item"
@click="item.func"
>
{{ item.label }}
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="close">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useRouterStore } from '@/pinia/modules/router'
import { useUserStore } from '@/pinia/modules/user'
defineOptions({
name: 'CommandMenu',
})
const router = useRouter()
const route = useRouter()
const userStore = useUserStore()
const routerStore = useRouterStore()
const dialogVisible = ref(false)
const searchInput = ref('')
const options = reactive([])
const deepMenus = (menus) => {
const arr = []
menus.forEach(menu => {
if (menu.children && menu.children.length > 0) {
arr.push(...deepMenus(menu.children))
} else {
if (menu.meta.title && menu.meta.title.indexOf(searchInput.value) > -1) {
arr.push({
label: menu.meta.title,
func: () => changeRouter(menu)
})
}
}
})
return arr
}
const addQuickMenu = () => {
const option = {
label: '跳转',
children: []
}
const menus = deepMenus(routerStore.asyncRouters[0].children)
option.children.push(...menus)
options.push(option)
}
const addQuickOption = () => {
const option = {
label: '操作',
children: []
}
const quickArr = [
{
label: '亮色主题',
func: () => changeMode('light')
}, {
label: '暗色主题',
func: () => changeMode('dark')
}, {
label: '退出登录',
func: () => userStore.LoginOut()
}
]
option.children.push(...quickArr.filter(item => item.label.indexOf(searchInput.value) > -1))
options.push(option)
}
addQuickMenu()
addQuickOption()
const open = () => {
dialogVisible.value = true
}
const changeRouter = (e) => {
const index = e.name
const query = {}
const params = {}
routerStore.routeMap[index]?.parameters &&
routerStore.routeMap[index]?.parameters.forEach((item) => {
if (item.type === 'query') {
query[item.key] = item.value
} else {
params[item.key] = item.value
}
})
if (index === route.name) return
if (e.name.indexOf('http://') > -1 || e.name.indexOf('https://') > -1) {
window.open(e.name)
} else {
router.push({ name: index, query, params })
}
dialogVisible.value = false
}
const changeMode = (e) => {
if (e === null) {
userStore.changeSideMode('dark')
return
}
userStore.changeSideMode(e)
}
const close = () => {
dialogVisible.value = false
}
defineExpose({ open })
watch(searchInput, () => {
options.length = 0
addQuickMenu()
addQuickOption()
})
</script>
<style lang="scss">
.overlay {
border-radius: 4px;
.el-dialog__header{
padding:0 !important;
margin-right:0 !important;
}
.el-dialog__body{
padding: 12px !important;
height: 50vh;
overflow: auto !important;
}
.quick-title{
margin-top: 8px;
font-size: 12px;
font-weight: 600;
color: #666;
}
.quick-input{
color: #666;
border-radius: 4px 4px 0 0;
border:none;
padding: 12px 16px;
box-sizing: border-box;
width: 100%;
font-size: 16px;
border-bottom: 1px solid #ddd;
}
.quick-item{
font-size: 14px;
padding: 8px;
margin: 4px 0;
&:hover{
cursor: pointer;
background: #eee;
border-radius: 4px;
}
}
}
</style>

6
src/utils/bus.js Normal file
View File

@ -0,0 +1,6 @@
// using ES6 modules
import mitt from 'mitt'
export const emitter = mitt()

View File

@ -0,0 +1,5 @@
import { emitter } from '@/utils/bus.js'
export const closeThisPage = () => {
emitter.emit('closeThisPage')
}

30
src/utils/date.js Normal file
View File

@ -0,0 +1,30 @@
// 对Date的扩展将 Date 转化为指定格式的String
// 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
// 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
// (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423
// (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
// eslint-disable-next-line no-extend-native
Date.prototype.Format = function(fmt) {
var o = {
'M+': this.getMonth() + 1, // 月份
'd+': this.getDate(), // 日
'h+': this.getHours(), // 小时
'm+': this.getMinutes(), // 分
's+': this.getSeconds(), // 秒
'q+': Math.floor((this.getMonth() + 3) / 3), // 季度
'S': this.getMilliseconds() // 毫秒
}
if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length)) }
for (var k in o) {
if (new RegExp('(' + k + ')').test(fmt)) { fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length))) }
}
return fmt
}
export function formatTimeToStr(times, pattern) {
var d = new Date(times).Format('yyyy-MM-dd hh:mm:ss')
if (pattern) {
d = new Date(times).Format(pattern)
}
return d.toLocaleString()
}

26
src/utils/dictionary.js Normal file
View File

@ -0,0 +1,26 @@
import { useDictionaryStore } from '@/pinia/modules/dictionary'
// 获取字典方法 使用示例 getDict('sex').then(res) 或者 async函数下 const res = await getDict('sex')
export const getDict = async(type) => {
const dictionaryStore = useDictionaryStore()
await dictionaryStore.getDictionary(type)
return dictionaryStore.dictionaryMap[type]
}
// 字典文字展示方法
export const showDictLabel = (
dict,
code,
keyCode = 'value',
valueCode = 'label'
) => {
if (!dict) {
return ''
}
const dictMap = {}
dict.forEach(item => {
if (Reflect.has(item, keyCode) && Reflect.has(item, valueCode)) {
dictMap[item[keyCode]] = item[valueCode]
}
})
return Reflect.has(dictMap, code) ? dictMap[code] : ''
}

3
src/utils/doc.js Normal file
View File

@ -0,0 +1,3 @@
export const toDoc = (url) => {
window.open(url, '_blank')
}

19
src/utils/downloadImg.js Normal file
View File

@ -0,0 +1,19 @@
export const downloadImage = (imgsrc, name) => { // 下载图片地址和图片名
var image = new Image()
image.setAttribute('crossOrigin', 'anonymous')
image.onload = function() {
var canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
var context = canvas.getContext('2d')
context.drawImage(image, 0, 0, image.width, image.height)
var url = canvas.toDataURL('image/png') // 得到图片的base64编码数据
var a = document.createElement('a') // 生成一个a元素
var event = new MouseEvent('click') // 创建一个单击事件
a.download = name || 'photo' // 设置图片名称
a.href = url // 将生成的URL设置为a.href属性
a.dispatchEvent(event) // 触发a的单击事件
}
image.src = imgsrc
}

View File

@ -0,0 +1,13 @@
export const fmtTitle = (title, now) => {
const reg = /\$\{(.+?)\}/
const reg_g = /\$\{(.+?)\}/g
const result = title.match(reg_g)
if (result) {
result.forEach((item) => {
const key = item.match(reg)[1]
const value = now.params[key] || now.query[key]
title = title.replace(item, value)
})
}
return title
}

67
src/utils/format.js Normal file
View File

@ -0,0 +1,67 @@
import { formatTimeToStr } from '@/utils/date'
import { getDict } from '@/utils/dictionary'
export const formatBoolean = (bool) => {
if (bool !== null) {
return bool ? '开启' : '关闭'
} else {
return ''
}
}
export const formatDate = (time) => {
if (time !== null && time !== '') {
var date = new Date(time)
return formatTimeToStr(date, 'yyyy-MM-dd hh:mm:ss')
} else {
return ''
}
}
export const formatOnlyDate = (time) => {
if (time !== null && time !== '') {
var date = new Date(time)
return formatTimeToStr(date, 'yyyy-MM-dd')
} else {
return ''
}
}
export const formatPriceType = (priceType) => {
switch (priceType) {
case 1: return '出厂价'
default: '未定义'
}
}
export const filterDict = (value, options) => {
const rowLabel = options && options.filter(item => item.value === value)
return rowLabel && rowLabel[0] && rowLabel[0].label
}
export const getDictFunc = async (type) => {
const dicts = await getDict(type)
return dicts
}
const path = import.meta.env.VITE_BASE_PATH + ':' + import.meta.env.VITE_SERVER_PORT + '/'
export const ReturnArrImg = (arr) => {
const imgArr = []
if (arr instanceof Array) { // 如果是数组类型
for (const arrKey in arr) {
if (arr[arrKey].slice(0, 4) !== 'http') {
imgArr.push(path + arr[arrKey])
} else {
imgArr.push(arr[arrKey])
}
}
} else { // 如果不是数组类型
if (arr.slice(0, 4) !== 'http') {
imgArr.push(path + arr)
} else {
imgArr.push(arr)
}
}
return imgArr
}
export const onDownloadFile = (url) => {
window.open(path + url)
}

101
src/utils/image.js Normal file
View File

@ -0,0 +1,101 @@
export default class ImageCompress {
constructor(file, fileSize, maxWH = 1920) {
this.file = file
this.fileSize = fileSize
this.maxWH = maxWH // 最大长宽
}
compress() {
// 压缩
const fileType = this.file.type
const fileSize = this.file.size / 1024
return new Promise(resolve => {
const reader = new FileReader()
reader.readAsDataURL(this.file)
reader.onload = () => {
const canvas = document.createElement('canvas')
const img = document.createElement('img')
img.src = reader.result
img.onload = () => {
const ctx = canvas.getContext('2d')
const _dWH = this.dWH(img.width, img.height, this.maxWH)
canvas.width = _dWH.width
canvas.height = _dWH.height
// 清空后, 重写画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
const newImgData = canvas.toDataURL(fileType, 0.90)
// 压缩宽高后的图像大小
const newImgSize = this.fileSizeKB(newImgData)
if (newImgSize > this.fileSize) {
console.log('图片尺寸太大!' + fileSize + ' >> ' + newImgSize)
}
const blob = this.dataURLtoBlob(newImgData, fileType)
const nfile = new File([blob], this.file.name)
resolve(nfile)
}
}
})
}
/**
* 长宽等比缩小
* 图像的一边(长或宽)为最大目标值
*/
dWH(srcW, srcH, dMax) {
const defaults = {
width: srcW,
height: srcH
}
if (Math.max(srcW, srcH) > dMax) {
if (srcW > srcH) {
defaults.width = dMax
defaults.height = Math.round(srcH * (dMax / srcW))
return defaults
} else {
defaults.height = dMax
defaults.width = Math.round(srcW * (dMax / srcH))
return defaults
}
} else {
return defaults
}
}
fileSizeKB(dataURL) {
let sizeKB = 0
sizeKB = Math.round((dataURL.split(',')[1].length * 3 / 4) / 1024)
return sizeKB
}
/**
* 转为Blob
*/
dataURLtoBlob(dataURL, fileType) {
const byteString = atob(dataURL.split(',')[1])
let mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0]
const ab = new ArrayBuffer(byteString.length)
const ia = new Uint8Array(ab)
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
if (fileType) {
mimeString = fileType
}
return new Blob([ab], { type: mimeString, lastModifiedDate: new Date() })
}
}
const path = import.meta.env.VITE_FILE_API + '/'
export const getUrl = (url) => url && url.slice(0, 4) !== 'http' ? path + url : url
export const isVideoExt = (url) => url.endsWith('.mp4') || url.endsWith('.mov') || url.endsWith('.webm') || url.endsWith('.ogg');
export const isVideoMime = (type) => type == 'video/mp4' || type == 'video/webm' || type == 'video/ogg';
export const isImageMime = (type) => type == 'image/jpeg' || type == 'image/png' || type == 'image/webp' || type == 'image/svg+xml';

9
src/utils/page.js Normal file
View File

@ -0,0 +1,9 @@
import { fmtTitle } from '@/utils/fmtRouterTitle'
import config from '@/core/config'
export default function getPageTitle(pageTitle, route) {
if (pageTitle) {
const title = fmtTitle(pageTitle, route)
return `${title} - ${config.appName}`
}
return `${config.appName}`
}

View File

@ -0,0 +1,36 @@
export const initDom = () => {
if (import.meta.env.MODE === 'development') {
document.onmousedown = function(e) {
if (e.shiftKey && e.altKey && e.button === 0) {
e.preventDefault()
sendRequestToOpenFileInEditor(getFilePath(e))
}
}
}
}
const getFilePath = (e) => {
let element = e
if (e.target) {
element = e.target
}
if (!element || !element.getAttribute) return null
if (element.getAttribute('code-location')) {
return element.getAttribute('code-location')
}
return getFilePath(element.parentNode)
}
const sendRequestToOpenFileInEditor = (filePath) => {
const protocol = window.location.protocol
? window.location.protocol
: 'http:'
const hostname = window.location.hostname
? window.location.hostname
: 'localhost'
const port = window.location.port ? window.location.port : '80'
fetch(`${protocol}//${hostname}:${port}/gvaPositionCode?filePath=${filePath}`)
.catch((error) => {
console.log(error)
})
}

145
src/utils/request.js Normal file
View File

@ -0,0 +1,145 @@
import axios from 'axios' // 引入axios
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/pinia/modules/user'
import router from '@/router/index'
import { ElLoading } from 'element-plus'
const service = axios.create({
baseURL: import.meta.env.VITE_BASE_API,
timeout: 99999
})
let activeAxios = 0
let timer
let loadingInstance
const showLoading = (option = {
target: null,
}) => {
const loadDom = document.getElementById('gva-base-load-dom')
activeAxios++
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
if (activeAxios > 0) {
if (!option.target) option.target = loadDom
loadingInstance = ElLoading.service(option)
}
}, 400)
}
const closeLoading = () => {
activeAxios--
if (activeAxios <= 0) {
clearTimeout(timer)
loadingInstance && loadingInstance.close()
}
}
// http request 拦截器
service.interceptors.request.use(
config => {
if (!config.donNotShowLoading) {
showLoading(config.loadingOption)
}
const userStore = useUserStore()
config.headers = {
'Content-Type': 'application/json',
'x-token': userStore.token,
'x-user-id': userStore.userInfo.ID,
...config.headers
}
return config
},
error => {
if (!error.config.donNotShowLoading) {
closeLoading()
}
ElMessage({
showClose: true,
message: error,
type: 'error'
})
return error
}
)
// http response 拦截器
service.interceptors.response.use(
response => {
const userStore = useUserStore()
if (!response.config.donNotShowLoading) {
closeLoading()
}
if (response.headers['new-token']) {
userStore.setToken(response.headers['new-token'])
}
if (response.data.code === 0 || response.headers.success === 'true') {
if (response.headers.msg) {
response.data.msg = decodeURI(response.headers.msg)
}
return response.data
} else {
ElMessage({
showClose: true,
message: response.data.msg || decodeURI(response.headers.msg),
type: 'error'
})
if (response.data.data && response.data.data.reload) {
userStore.token = ''
window.localStorage.removeItem('token')
router.push({ name: 'Login', replace: true })
}
return response.data.msg ? response.data : response
}
},
error => {
if (!error.config.donNotShowLoading) {
closeLoading()
}
if (!error.response) {
ElMessageBox.confirm(`
<p>检测到请求错误</p>
<p>${error}</p>
`, '请求报错', {
dangerouslyUseHTMLString: true,
distinguishCancelAndClose: true,
confirmButtonText: '稍后重试',
cancelButtonText: '取消'
})
return
}
switch (error.response.status) {
case 500:
ElMessageBox.confirm(`
<p>检测到接口错误${error}</p>
<p>错误码<span style="color:red"> 500 </span>panic使</p>
`, '接口报错', {
dangerouslyUseHTMLString: true,
distinguishCancelAndClose: true,
confirmButtonText: '清理缓存',
cancelButtonText: '取消'
})
.then(() => {
const userStore = useUserStore()
userStore.ClearStorage()
router.push({ name: 'Login', replace: true })
})
break
case 404:
ElMessageBox.confirm(`
<p>检测到接口错误${error}</p>
<p>错误码<span style="color:red"> 404 </span>api--</p>
`, '接口报错', {
dangerouslyUseHTMLString: true,
distinguishCancelAndClose: true,
confirmButtonText: '我知道了',
cancelButtonText: '取消'
})
break
}
return error
}
)
export default service

29
src/utils/stringFun.js Normal file
View File

@ -0,0 +1,29 @@
/* eslint-disable */
export const toUpperCase = (str) => {
if (str[0]) {
return str.replace(str[0], str[0].toUpperCase())
} else {
return ''
}
}
export const toLowerCase = (str) => {
if (str[0]) {
return str.replace(str[0], str[0].toLowerCase())
} else {
return ''
}
}
// 驼峰转换下划线
export const toSQLLine = (str) => {
if (str === 'ID') return 'ID'
return str.replace(/([A-Z])/g, "_$1").toLowerCase();
}
// 下划线转换驼峰
export const toHump = (name) => {
return name.replace(/\_(\w)/g, function(all, letter) {
return letter.toUpperCase();
});
}

17
src/view/about/index.vue Normal file
View File

@ -0,0 +1,17 @@
<template>
<div>
<el-row :gutter="10">
<el-col :span="24">
<div>
email: phpcrazy@163.com
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
defineOptions({
name: 'About'
})
</script>

View File

@ -0,0 +1,458 @@
<template>
<div>
<div class="gva-search-box">
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline" :rules="searchRule"
@keyup.enter="onSubmit">
<el-form-item label="创建日期" prop="createdAt">
<template #label>
<span>
创建日期
<el-tooltip content="搜索范围是开始日期(包含)至结束日期(不包含)">
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-date-picker v-model="searchInfo.startCreatedAt" type="datetime" placeholder="开始日期"
:disabled-date="time=> searchInfo.endCreatedAt ? time.getTime() > searchInfo.endCreatedAt.getTime() : false"></el-date-picker>
<el-date-picker v-model="searchInfo.endCreatedAt" type="datetime" placeholder="结束日期"
:disabled-date="time=> searchInfo.startCreatedAt ? time.getTime() < searchInfo.startCreatedAt.getTime() : false"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
<el-button icon="refresh" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" icon="plus" @click="openDialog">新增</el-button>
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length"
@click="onDelete">删除</el-button>
</div>
<el-table ref="multipleTable" style="width: 100%" tooltip-effect="dark" :data="tableData" row-key="ID"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column align="left" label="ID" prop="ID" width="80" />
<el-table-column align="left" label="产品" prop="productId" width="120">
<template #default="scope">{{ formatProduct(scope.row.productId) }}</template>
</el-table-column>
<el-table-column align="left" label="产品价格Id" prop="productPriceId" width="100" />
<el-table-column align="left" label="数据源Id" prop="sourceId" width="100" />
<el-table-column align="left" label="公司" prop="companyName" width="240" />
<el-table-column align="left" label="品牌" prop="producer" width="120" />
<el-table-column align="left" label="价格" prop="price" width="80" />
<el-table-column align="left" label="单位" prop="priceUnit" width="60" />
<el-table-column align="left" label="发布日期" width="100">
<template #default="scope">{{ formatOnlyDate(scope.row.publishDate) }}</template>
</el-table-column>
<el-table-column align="left" label="创建时间" width="180">
<template #default="scope">{{ formatDate(scope.row.createdAt) }}</template>
</el-table-column>
<el-table-column align="left" label="操作" fixed="right" min-width="240">
<template #default="scope">
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)">
<el-icon style="margin-right: 5px">
<InfoFilled />
</el-icon>
查看详情
</el-button>
<el-button type="primary" link icon="edit" class="table-button"
@click="updateCompanyPriceFunc(scope.row)">变更</el-button>
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination layout="total, sizes, prev, pager, next, jumper" :current-page="page" :page-size="pageSize"
:page-sizes="[10, 30, 50, 100]" :total="total" @current-change="handleCurrentChange"
@size-change="handleSizeChange" />
</div>
</div>
<el-drawer size="800" v-model="dialogFormVisible" :show-close="false" :before-close="closeDialog">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">{{ type==='create'?'添加':'修改' }}</span>
<div>
<el-button type="primary" @click="enterDialog"> </el-button>
<el-button @click="closeDialog"> </el-button>
</div>
</div>
</template>
<el-form :model="formData" label-position="top" ref="elFormRef" :rules="rule" label-width="80px">
<el-form-item label="产品Id:" prop="productId">
<el-input v-model.number="formData.productId" :clearable="true" placeholder="请输入产品Id" />
</el-form-item>
<el-form-item label="产品价格Id:" prop="productPriceId">
<el-input v-model.number="formData.productPriceId" :clearable="true" placeholder="请输入产品价格Id" />
</el-form-item>
<el-form-item label="数据源Id:" prop="sourceId">
<el-input v-model.number="formData.sourceId" :clearable="true" placeholder="请输入数据源Id" />
</el-form-item>
<el-form-item label="公司:" prop="companyName">
<el-input v-model="formData.companyName" :clearable="true" placeholder="请输入公司" />
</el-form-item>
<el-form-item label="品牌:" prop="producer">
<el-input v-model="formData.producer" :clearable="true" placeholder="请输入品牌" />
</el-form-item>
<el-form-item label="价格:" prop="price">
<el-input-number v-model="formData.price" style="width:100%" :precision="2" :clearable="true" />
</el-form-item>
<el-form-item label="单位:" prop="priceUnit">
<el-input v-model="formData.priceUnit" :clearable="true" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="发布日期:" prop="publishDate">
<el-date-picker v-model="formData.publishDate" type="date" style="width:100%" placeholder="选择日期" :clearable="true" />
</el-form-item>
</el-form>
</el-drawer>
<el-drawer size="800" v-model="detailShow" :before-close="closeDetailShow" destroy-on-close>
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">查看详情</span>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="ID">
{{ formData.ID }}
</el-descriptions-item>
<el-descriptions-item label="产品">
{{ formatProduct(formData.productId) }}
</el-descriptions-item>
<el-descriptions-item label="产品价格Id">
{{ formData.productPriceId }}
</el-descriptions-item>
<el-descriptions-item label="数据源Id">
{{ formData.sourceId }}
</el-descriptions-item>
<el-descriptions-item label="公司">
{{ formData.companyName }}
</el-descriptions-item>
<el-descriptions-item label="品牌">
{{ formData.producer }}
</el-descriptions-item>
<el-descriptions-item label="价格">
{{ formData.price }}
</el-descriptions-item>
<el-descriptions-item label="单位">
{{ formData.priceUnit }}
</el-descriptions-item>
<el-descriptions-item label="发布日期">
{{ formatOnlyDate(formData.publishDate) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(formData.createdAt) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDate(formData.updatedAt) }}
</el-descriptions-item>
</el-descriptions>
</el-drawer>
</div>
</template>
<script setup>
import { getProductList } from '@/api/product'
import {
createCompanyPrice,
deleteCompanyPrice,
deleteCompanyPriceByIds,
updateCompanyPrice,
findCompanyPrice,
getCompanyPriceList
} from '@/api/companyPrice'
//
import { formatDate, formatOnlyDate } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive } from 'vue'
defineOptions({
name: 'CompanyPrice'
})
//
const formData = ref({
productId: 0,
productPriceId: 0,
sourceId: 0,
companyName: '',
producer: '',
price: 0,
priceUnit: '',
publishDate: new Date(),
})
//
const rule = reactive({
})
const searchRule = reactive({
createdAt: [
{ validator: (rule, value, callback) => {
if (searchInfo.value.startCreatedAt && !searchInfo.value.endCreatedAt) {
callback(new Error('请填写结束日期'))
} else if (!searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt) {
callback(new Error('请填写开始日期'))
} else if (searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt && (searchInfo.value.startCreatedAt.getTime() === searchInfo.value.endCreatedAt.getTime() || searchInfo.value.startCreatedAt.getTime() > searchInfo.value.endCreatedAt.getTime())) {
callback(new Error('开始日期应当早于结束日期'))
} else {
callback()
}
}, trigger: 'change' }
],
})
const elFormRef = ref()
const elSearchFormRef = ref()
// =========== ===========
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
const searchInfo = ref({})
const productList = ref([])
const formatProduct = (id) => {
for (let i = 0; i < productList.value.length; i++) {
if (productList.value[i].ID === id) {
return productList.value[i].name
}
}
return id
}
//
const onReset = () => {
searchInfo.value = {}
getTableData()
}
//
const onSubmit = () => {
elSearchFormRef.value?.validate(async(valid) => {
if (!valid) return
page.value = 1
pageSize.value = 10
getTableData()
})
}
//
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
//
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
const getProductData = async() => {
const resp = await getProductList({ page: 1, pageSize: 100 })
if (resp.code === 0 && resp.data.list) {
productList.value = resp.data.list
}
}
getProductData()
//
const getTableData = async() => {
const table = await getCompanyPriceList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
// ============== ===============
//
const setOptions = async() => {
}
//
setOptions()
//
const multipleSelection = ref([])
//
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
//
const deleteRow = (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteCompanyPriceFunc(row)
})
}
//
const onDelete = async() => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
const IDs = []
if (multipleSelection.value.length === 0) {
ElMessage({
type: 'warning',
message: '请选择要删除的数据'
})
return
}
multipleSelection.value &&
multipleSelection.value.map(item => {
IDs.push(item.ID)
})
const res = await deleteCompanyPriceByIds({ IDs })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === IDs.length && page.value > 1) {
page.value--
}
getTableData()
}
})
}
//
const type = ref('')
//
const updateCompanyPriceFunc = async(row) => {
const res = await findCompanyPrice({ ID: row.ID })
type.value = 'update'
if (res.code === 0) {
formData.value = res.data.recompanyPrice
dialogFormVisible.value = true
}
}
//
const deleteCompanyPriceFunc = async(row) => {
const res = await deleteCompanyPrice({ ID: row.ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
//
const dialogFormVisible = ref(false)
//
const detailShow = ref(false)
//
const openDetailShow = () => {
detailShow.value = true
}
//
const getDetails = async(row) => {
//
const res = await findCompanyPrice({ ID: row.ID })
if (res.code === 0) {
formData.value = res.data.recompanyPrice
openDetailShow()
}
}
//
const closeDetailShow = () => {
detailShow.value = false
formData.value = {
productId: 0,
productPriceId: 0,
sourceId: 0,
companyName: '',
producer: '',
price: 0,
priceUnit: '',
publishDate: new Date(),
}
}
//
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
//
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
productId: 0,
productPriceId: 0,
sourceId: 0,
companyName: '',
producer: '',
price: 0,
priceUnit: '',
publishDate: new Date(),
}
}
//
const enterDialog = async() => {
elFormRef.value?.validate(async(valid) => {
if (!valid) return
let res
switch (type.value) {
case 'create':
res = await createCompanyPrice(formData.value)
break
case 'update':
res = await updateCompanyPrice(formData.value)
break
default:
res = await createCompanyPrice(formData.value)
break
}
if (res.code === 0) {
ElMessage({
type: 'success',
message: '创建/更改成功'
})
closeDialog()
getTableData()
}
})
}
</script>
<style>
</style>

View File

@ -0,0 +1,123 @@
<template>
<div>
<div class="gva-form-box">
<el-form :model="formData" ref="elFormRef" label-position="right" :rules="rule" label-width="80px">
<el-form-item label="产品Id:" prop="productId">
<el-input v-model.number="formData.productId" :clearable="true" placeholder="请输入" />
</el-form-item>
<el-form-item label="产品价格Id:" prop="productPriceId">
<el-input v-model.number="formData.productPriceId" :clearable="true" placeholder="请输入" />
</el-form-item>
<el-form-item label="数据源Id:" prop="sourceId">
<el-input v-model.number="formData.sourceId" :clearable="true" placeholder="请输入" />
</el-form-item>
<el-form-item label="公司:" prop="companyName">
<el-input v-model="formData.companyName" :clearable="true" placeholder="请输入公司" />
</el-form-item>
<el-form-item label="品牌:" prop="producer">
<el-input v-model="formData.producer" :clearable="true" placeholder="请输入品牌" />
</el-form-item>
<el-form-item label="价格:" prop="price">
<el-input-number v-model="formData.price" :precision="2" :clearable="true"></el-input-number>
</el-form-item>
<el-form-item label="单位:" prop="priceUnit">
<el-input v-model="formData.priceUnit" :clearable="true" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="发布日期:" prop="publishDate">
<el-date-picker v-model="formData.publishDate" type="date" placeholder="选择日期" :clearable="true"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="save">保存</el-button>
<el-button type="primary" @click="back">返回</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import {
createCompanyPrice,
updateCompanyPrice,
findCompanyPrice
} from '@/api/companyPrice'
defineOptions({
name: 'CompanyPriceForm'
})
//
import { getDictFunc } from '@/utils/format'
import { useRoute, useRouter } from "vue-router"
import { ElMessage } from 'element-plus'
import { ref, reactive } from 'vue'
const route = useRoute()
const router = useRouter()
const type = ref('')
const formData = ref({
productId: 0,
productPriceId: 0,
sourceId: 0,
companyName: '',
producer: '',
price: 0,
priceUnit: '',
publishDate: new Date(),
})
//
const rule = reactive({
})
const elFormRef = ref()
//
const init = async() => {
// urlID find createupdate idurl
if (route.query.id) {
const res = await findCompanyPrice({ ID: route.query.id })
if (res.code === 0) {
formData.value = res.data.recompanyPrice
type.value = 'update'
}
} else {
type.value = 'create'
}
}
init()
//
const save = async() => {
elFormRef.value?.validate(async(valid) => {
if (!valid) return
let res
switch (type.value) {
case 'create':
res = await createCompanyPrice(formData.value)
break
case 'update':
res = await updateCompanyPrice(formData.value)
break
default:
res = await createCompanyPrice(formData.value)
break
}
if (res.code === 0) {
ElMessage({
type: 'success',
message: '创建/更改成功'
})
}
})
}
//
const back = () => {
router.go(-1)
}
</script>
<style>
</style>

View File

@ -0,0 +1,581 @@
<template>
<div>
<div class="gva-search-box">
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline" :rules="searchRule"
@keyup.enter="onSubmit">
<el-form-item label="状态:" prop="status">
<el-select v-model="searchInfo.status" placeholder="请选择" :clearable="true">
<el-option v-for="item in statusOptions" :key="item.key" :label="item.label" :value="item.key" />
</el-select>
</el-form-item>
<el-form-item label="创建日期" prop="createdAt">
<template #label>
<span>
创建日期
<el-tooltip content="搜索范围是开始日期(包含)至结束日期(不包含)">
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-date-picker v-model="searchInfo.startCreatedAt" type="datetime" placeholder="开始日期"
:disabled-date="time=> searchInfo.endCreatedAt ? time.getTime() > searchInfo.endCreatedAt.getTime() : false"></el-date-picker>
<el-date-picker v-model="searchInfo.endCreatedAt" type="datetime" placeholder="结束日期"
:disabled-date="time=> searchInfo.startCreatedAt ? time.getTime() < searchInfo.startCreatedAt.getTime() : false"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
<el-button icon="refresh" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" icon="plus" @click="openDialog">新增</el-button>
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length"
@click="onDelete">删除</el-button>
<el-button icon="check" style="margin-left: 10px;" :disabled="!multipleSelection.length"
@click="onSyncClick">重新同步</el-button>
</div>
<el-table ref="multipleTable" style="width: 100%" tooltip-effect="dark" :data="tableData" row-key="ID"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column align="left" label="ID" prop="ID" width="80" />
<el-table-column align="left" label="产品Code" prop="code" width="100" />
<el-table-column align="left" label="产品名称" prop="name" width="120" />
<el-table-column align="left" label="规格" prop="norm" width="300" />
<el-table-column align="left" label="品牌" prop="producer" width="120" />
<el-table-column align="left" label="价格类型" prop="priceType" width="120" />
<el-table-column align="left" label="单位" prop="priceUnit" width="80" />
<el-table-column align="left" label="价格" prop="price" width="80" />
<el-table-column align="left" label="地区" prop="areaDetail" width="120" />
<el-table-column align="left" label="公司" prop="dealer" width="240" />
<el-table-column align="left" label="发布日期" prop="publishDate" width="100" />
<el-table-column align="left" label="数据唯一值" prop="uniqueCode" width="300" />
<el-table-column align="left" label="抓取时间" prop="crawlTime" width="180" />
<el-table-column align="left" label="状态" prop="status" width="80">
<template #default="scope">{{ formatCrawlStatus(scope.row.status) }}</template>
</el-table-column>
<el-table-column align="left" label="同步失败原因" prop="syncError" width="240" />
<el-table-column align="left" label="创建时间" width="180">
<template #default="scope">{{ formatDate(scope.row.createdAt) }}</template>
</el-table-column>
<el-table-column align="left" label="操作" fixed="right" min-width="320">
<template #default="scope">
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)">
<el-icon style="margin-right: 5px">
<InfoFilled />
</el-icon>
查看
</el-button>
<el-button type="primary" link icon="edit" class="table-button"
@click="updateCrawl100ppiFunc(scope.row)">变更</el-button>
<el-button type="primary" link icon="check" @click="syncRow(scope.row)">重新同步</el-button>
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination layout="total, sizes, prev, pager, next, jumper" :current-page="page" :page-size="pageSize"
:page-sizes="[10, 30, 50, 100]" :total="total" @current-change="handleCurrentChange"
@size-change="handleSizeChange" />
</div>
</div>
<el-drawer size="800" v-model="dialogFormVisible" :show-close="false" :before-close="closeDialog">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">{{ type==='create'?'添加':'修改' }}</span>
<div>
<el-button type="primary" @click="enterDialog"> </el-button>
<el-button @click="closeDialog"> </el-button>
</div>
</div>
</template>
<el-form :model="formData" label-position="top" ref="elFormRef" :rules="rule" label-width="80px">
<el-form-item label="产品Code:" prop="code">
<el-input v-model="formData.code" :clearable="true" placeholder="请输入产品Code" />
</el-form-item>
<el-form-item label="产品名称:" prop="name">
<el-input v-model="formData.name" :clearable="true" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="规格:" prop="norm">
<el-input v-model="formData.norm" :clearable="true" placeholder="请输入规格" />
</el-form-item>
<el-form-item label="品牌:" prop="producer">
<el-input v-model="formData.producer" :clearable="true" placeholder="请输入品牌" />
</el-form-item>
<el-form-item label="价格类型:" prop="priceType">
<el-input v-model="formData.priceType" :clearable="true" placeholder="请输入价格类型" />
</el-form-item>
<el-form-item label="单位:" prop="priceUnit">
<el-input v-model="formData.priceUnit" :clearable="true" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="价格:" prop="price">
<el-input v-model="formData.price" :clearable="true" placeholder="请输入价格" />
</el-form-item>
<el-form-item label="地区:" prop="areaDetail">
<el-input v-model="formData.areaDetail" :clearable="true" placeholder="请输入地区" />
</el-form-item>
<el-form-item label="公司:" prop="dealer">
<el-input v-model="formData.dealer" :clearable="true" placeholder="请输入公司" />
</el-form-item>
<el-form-item label="发布日期:" prop="publishDate">
<el-input v-model="formData.publishDate" :clearable="true" placeholder="请输入发布日期" />
</el-form-item>
<el-form-item label="数据唯一值:" prop="uniqueCode">
<el-input v-model="formData.uniqueCode" :clearable="true" placeholder="请输入数据唯一值" />
</el-form-item>
<el-form-item label="抓取时间:" prop="crawlTime">
<el-input v-model="formData.crawlTime" :clearable="true" placeholder="请输入抓取时间" />
</el-form-item>
<el-form-item label="状态:" prop="status">
<el-input v-model.number="formData.status" :clearable="true" placeholder="请输入状态" />
</el-form-item>
<el-form-item label="同步失败原因:" prop="syncError">
<el-input v-model="formData.syncError" :clearable="true" placeholder="请输入同步失败原因" />
</el-form-item>
</el-form>
</el-drawer>
<el-drawer size="800" v-model="detailShow" :before-close="closeDetailShow" destroy-on-close>
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">查看详情</span>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="ID">
{{ formData.ID }}
</el-descriptions-item>
<el-descriptions-item label="产品Code">
{{ formData.code }}
</el-descriptions-item>
<el-descriptions-item label="产品名称">
{{ formData.name }}
</el-descriptions-item>
<el-descriptions-item label="规格">
{{ formData.norm }}
</el-descriptions-item>
<el-descriptions-item label="品牌">
{{ formData.producer }}
</el-descriptions-item>
<el-descriptions-item label="价格类型">
{{ formData.priceType }}
</el-descriptions-item>
<el-descriptions-item label="单位">
{{ formData.priceUnit }}
</el-descriptions-item>
<el-descriptions-item label="价格">
{{ formData.price }}
</el-descriptions-item>
<el-descriptions-item label="地区">
{{ formData.areaDetail }}
</el-descriptions-item>
<el-descriptions-item label="公司">
{{ formData.dealer }}
</el-descriptions-item>
<el-descriptions-item label="发布日期">
{{ formData.publishDate }}
</el-descriptions-item>
<el-descriptions-item label="数据唯一值">
{{ formData.uniqueCode }}
</el-descriptions-item>
<el-descriptions-item label="抓取时间">
{{ formData.crawlTime }}
</el-descriptions-item>
<el-descriptions-item label="状态">
{{ formatCrawlStatus(formData.status) }}
</el-descriptions-item>
<el-descriptions-item label="同步失败原因">
{{ formData.syncError }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(formData.createdAt) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDate(formData.updatedAt) }}
</el-descriptions-item>
</el-descriptions>
</el-drawer>
</div>
</template>
<script setup>
import {
createCrawl100ppi,
deleteCrawl100ppi,
syncCrawl100ppi,
deleteCrawl100ppiByIds,
updateCrawl100ppi,
findCrawl100ppi,
getCrawl100ppiList
} from '@/api/crawl100ppi'
//
import { getDictFunc, formatDate, filterDict, ReturnArrImg, onDownloadFile } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive } from 'vue'
defineOptions({
name: 'Crawl100ppi'
})
const statusOptions = [
{ key: 1, label: '未同步' },
{ key: 2, label: '同步成功' },
{ key: 3, label: '同步失败' },
]
const formatCrawlStatus = (status) => {
let item
statusOptions.forEach((option, index) => {
if (option.key === status) {
item = option
return
}
})
if (item) {
return item.label
}
return ''
}
//
const formData = ref({
code: '',
name: '',
norm: '',
producer: '',
priceType: '',
priceUnit: '',
price: '',
areaDetail: '',
dealer: '',
publishDate: '',
uniqueCode: '',
crawlTime: '',
status: 0,
syncError: '',
})
//
const rule = reactive({
})
const searchRule = reactive({
createdAt: [
{ validator: (rule, value, callback) => {
if (searchInfo.value.startCreatedAt && !searchInfo.value.endCreatedAt) {
callback(new Error('请填写结束日期'))
} else if (!searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt) {
callback(new Error('请填写开始日期'))
} else if (searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt && (searchInfo.value.startCreatedAt.getTime() === searchInfo.value.endCreatedAt.getTime() || searchInfo.value.startCreatedAt.getTime() > searchInfo.value.endCreatedAt.getTime())) {
callback(new Error('开始日期应当早于结束日期'))
} else {
callback()
}
}, trigger: 'change' }
],
})
const elFormRef = ref()
const elSearchFormRef = ref()
// =========== ===========
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
const searchInfo = ref({})
//
const onReset = () => {
searchInfo.value = {}
getTableData()
}
//
const onSubmit = () => {
elSearchFormRef.value?.validate(async(valid) => {
if (!valid) return
page.value = 1
pageSize.value = 10
getTableData()
})
}
//
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
//
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
//
const getTableData = async() => {
const table = await getCrawl100ppiList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
// ============== ===============
//
const setOptions = async() => {
}
//
setOptions()
//
const multipleSelection = ref([])
//
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
//
const deleteRow = (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteCrawl100ppiFunc(row)
})
}
//
const onDelete = async() => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
const IDs = []
if (multipleSelection.value.length === 0) {
ElMessage({
type: 'warning',
message: '请选择要删除的数据'
})
return
}
multipleSelection.value &&
multipleSelection.value.map(item => {
IDs.push(item.ID)
})
const res = await deleteCrawl100ppiByIds({ IDs })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === IDs.length && page.value > 1) {
page.value--
}
getTableData()
}
})
}
//
const syncRow = (row) => {
ElMessageBox.confirm('确定要同步吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
syncCrawl100ppiFunc(row)
})
}
//
const onSyncClick = async() => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
const IDs = []
if (multipleSelection.value.length === 0) {
ElMessage({
type: 'warning',
message: '请选择要同步的数据'
})
return
}
multipleSelection.value &&
multipleSelection.value.map(item => {
IDs.push(item.ID)
})
const res = await syncCrawl100ppi({ IDs })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '同步操作成功'
})
if (tableData.value.length === IDs.length && page.value > 1) {
page.value--
}
getTableData()
}
})
}
//
const type = ref('')
//
const updateCrawl100ppiFunc = async(row) => {
const res = await findCrawl100ppi({ ID: row.ID })
type.value = 'update'
if (res.code === 0) {
formData.value = res.data.recrawl100ppi
dialogFormVisible.value = true
}
}
//
const deleteCrawl100ppiFunc = async(row) => {
const res = await deleteCrawl100ppi({ ID: row.ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
//
const syncCrawl100ppiFunc = async(row) => {
const res = await syncCrawl100ppi({ IDs: [row.ID] })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '同步操作成功'
})
getTableData()
}
}
//
const dialogFormVisible = ref(false)
//
const detailShow = ref(false)
//
const openDetailShow = () => {
detailShow.value = true
}
//
const getDetails = async(row) => {
//
const res = await findCrawl100ppi({ ID: row.ID })
if (res.code === 0) {
formData.value = res.data.recrawl100ppi
openDetailShow()
}
}
//
const closeDetailShow = () => {
detailShow.value = false
formData.value = {
code: '',
name: '',
norm: '',
producer: '',
priceType: '',
priceUnit: '',
price: '',
areaDetail: '',
dealer: '',
publishDate: '',
uniqueCode: '',
crawlTime: '',
status: 0,
syncError: '',
}
}
//
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
//
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
code: '',
name: '',
norm: '',
producer: '',
priceType: '',
priceUnit: '',
price: '',
areaDetail: '',
dealer: '',
publishDate: '',
uniqueCode: '',
crawlTime: '',
status: 0,
syncError: '',
}
}
//
const enterDialog = async() => {
elFormRef.value?.validate(async(valid) => {
if (!valid) return
let res
switch (type.value) {
case 'create':
res = await createCrawl100ppi(formData.value)
break
case 'update':
res = await updateCrawl100ppi(formData.value)
break
default:
res = await createCrawl100ppi(formData.value)
break
}
if (res.code === 0) {
ElMessage({
type: 'success',
message: '创建/更改成功'
})
closeDialog()
getTableData()
}
})
}
</script>
<style>
</style>

View File

@ -0,0 +1,147 @@
<template>
<div>
<div class="gva-form-box">
<el-form :model="formData" ref="elFormRef" label-position="right" :rules="rule" label-width="80px">
<el-form-item label="产品Code:" prop="code">
<el-input v-model="formData.code" :clearable="true" placeholder="请输入产品Code" />
</el-form-item>
<el-form-item label="产品名称:" prop="name">
<el-input v-model="formData.name" :clearable="true" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="规格:" prop="norm">
<el-input v-model="formData.norm" :clearable="true" placeholder="请输入规格" />
</el-form-item>
<el-form-item label="品牌:" prop="producer">
<el-input v-model="formData.producer" :clearable="true" placeholder="请输入品牌" />
</el-form-item>
<el-form-item label="价格类型:" prop="priceType">
<el-input v-model="formData.priceType" :clearable="true" placeholder="请输入价格类型" />
</el-form-item>
<el-form-item label="单位:" prop="priceUnit">
<el-input v-model="formData.priceUnit" :clearable="true" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="价格:" prop="price">
<el-input v-model="formData.price" :clearable="true" placeholder="请输入价格" />
</el-form-item>
<el-form-item label="地区:" prop="areaDetail">
<el-input v-model="formData.areaDetail" :clearable="true" placeholder="请输入地区" />
</el-form-item>
<el-form-item label="公司:" prop="dealer">
<el-input v-model="formData.dealer" :clearable="true" placeholder="请输入公司" />
</el-form-item>
<el-form-item label="发布日期:" prop="publishDate">
<el-input v-model="formData.publishDate" :clearable="true" placeholder="请输入发布日期" />
</el-form-item>
<el-form-item label="数据唯一值:" prop="uniqueCode">
<el-input v-model="formData.uniqueCode" :clearable="true" placeholder="请输入数据唯一值" />
</el-form-item>
<el-form-item label="抓取时间:" prop="crawlTime">
<el-input v-model="formData.crawlTime" :clearable="true" placeholder="请输入抓取时间" />
</el-form-item>
<el-form-item label="状态:" prop="status">
<el-input v-model.number="formData.status" :clearable="true" placeholder="请输入" />
</el-form-item>
<el-form-item label="同步失败原因:" prop="syncError">
<el-input v-model="formData.syncError" :clearable="true" placeholder="请输入同步失败原因" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="save">保存</el-button>
<el-button type="primary" @click="back">返回</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import {
createCrawl100ppi,
updateCrawl100ppi,
findCrawl100ppi
} from '@/api/crawl100ppi'
defineOptions({
name: 'Crawl100ppiForm'
})
//
import { getDictFunc } from '@/utils/format'
import { useRoute, useRouter } from "vue-router"
import { ElMessage } from 'element-plus'
import { ref, reactive } from 'vue'
const route = useRoute()
const router = useRouter()
const type = ref('')
const formData = ref({
code: '',
name: '',
norm: '',
producer: '',
priceType: '',
priceUnit: '',
price: '',
areaDetail: '',
dealer: '',
publishDate: '',
uniqueCode: '',
crawlTime: '',
status: 0,
syncError: '',
})
//
const rule = reactive({
})
const elFormRef = ref()
//
const init = async() => {
// urlID find createupdate idurl
if (route.query.id) {
const res = await findCrawl100ppi({ ID: route.query.id })
if (res.code === 0) {
formData.value = res.data.recrawl100ppi
type.value = 'update'
}
} else {
type.value = 'create'
}
}
init()
//
const save = async() => {
elFormRef.value?.validate(async(valid) => {
if (!valid) return
let res
switch (type.value) {
case 'create':
res = await createCrawl100ppi(formData.value)
break
case 'update':
res = await updateCrawl100ppi(formData.value)
break
default:
res = await createCrawl100ppi(formData.value)
break
}
if (res.code === 0) {
ElMessage({
type: 'success',
message: '创建/更改成功'
})
}
})
}
//
const back = () => {
router.go(-1)
}
</script>
<style>
</style>

View File

@ -0,0 +1,409 @@
<template>
<div>
<div class="gva-search-box">
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline" @keyup.enter="onSubmit">
<el-form-item label="字段名" prop="fieldName">
<template #label>
<span>字段名</span>
</template>
<el-input v-model="searchInfo.fieldName" type="input" />
</el-form-item>
<el-form-item label="是否同步:" prop="isSync">
<el-select v-model="searchInfo.isSync" placeholder="请选择" :clearable="true">
<el-option v-for="item in ['yes', 'no']" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
<el-button icon="refresh" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" icon="plus" @click="openDialog">新增</el-button>
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length" @click="onDelete">删除</el-button>
</div>
<el-table
ref="multipleTable"
style="width: 100%"
tooltip-effect="dark"
:data="tableData"
row-key="id"
@selection-change="handleSelectionChange"
@sort-change="sortChange"
>
<el-table-column type="selection" width="55" />
<el-table-column sortable align="left" label="ID" prop="id" width="80" />
<el-table-column align="left" label="字段名" prop="fieldName" width="120" />
<el-table-column align="left" label="原始值" prop="fieldSource" width="300" />
<el-table-column align="left" label="替换后的值" prop="fieldReplace" width="200" />
<el-table-column align="left" label="是否同步" prop="isSync" width="120" />
<el-table-column align="left" label="操作" fixed="right" min-width="240">
<template #default="scope">
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)">
<el-icon style="margin-right: 5px"><InfoFilled /></el-icon>
查看详情
</el-button>
<el-button type="primary" link icon="edit" class="table-button" @click="updateCrawlFieldReplaceFunc(scope.row)">变更</el-button>
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-drawer size="800" v-model="dialogFormVisible" :show-close="false" :before-close="closeDialog">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">{{ type==='create'?'添加':'修改' }}</span>
<div>
<el-button type="primary" @click="enterDialog"> </el-button>
<el-button @click="closeDialog"> </el-button>
</div>
</div>
</template>
<el-form :model="formData" label-position="top" ref="elFormRef" :rules="rule" label-width="80px">
<el-form-item label="字段名:" prop="fieldName">
<el-input v-model="formData.fieldName" :clearable="true" placeholder="请输入字段名" />
</el-form-item>
<el-form-item label="原始值:" prop="fieldSource">
<el-input v-model="formData.fieldSource" :clearable="true" placeholder="请输入原始值" />
</el-form-item>
<el-form-item label="替换后的值:" prop="fieldReplace">
<el-input v-model="formData.fieldReplace" :clearable="true" placeholder="请输入替换后的值" />
</el-form-item>
<el-form-item label="是否同步:" prop="isSync">
<el-select v-model="formData.isSync" placeholder="请选择是否同步" style="width:100%" :clearable="true" >
<el-option v-for="item in ['yes', 'no']" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-form>
</el-drawer>
<el-drawer size="800" v-model="detailShow" :before-close="closeDetailShow" destroy-on-close>
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">查看详情</span>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="ID">
{{ formData.id }}
</el-descriptions-item>
<el-descriptions-item label="字段名">
{{ formData.fieldName }}
</el-descriptions-item>
<el-descriptions-item label="原始值">
{{ formData.fieldSource }}
</el-descriptions-item>
<el-descriptions-item label="替换后的值">
{{ formData.fieldReplace }}
</el-descriptions-item>
<el-descriptions-item label="是否同步">
{{ formData.isSync }}
</el-descriptions-item>
</el-descriptions>
</el-drawer>
</div>
</template>
<script setup>
import {
createCrawlFieldReplace,
deleteCrawlFieldReplace,
deleteCrawlFieldReplaceByIds,
updateCrawlFieldReplace,
findCrawlFieldReplace,
getCrawlFieldReplaceList
} from '@/api/crawlFieldReplace'
//
// import { getDictFunc, formatDate, formatBoolean, filterDict, ReturnArrImg, onDownloadFile } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive } from 'vue'
defineOptions({
name: 'CrawlFieldReplace'
})
//
const formData = ref({
id: 0,
fieldName: '',
fieldReplace: '',
fieldSource: '',
isSync: '',
})
//
const rule = reactive({
fieldName: [{
required: true,
message: '',
trigger: ['input', 'blur'],
},
{
whitespace: true,
message: '不能只输入空格',
trigger: ['input', 'blur'],
}
],
id: [{
required: true,
message: '',
trigger: ['input', 'blur'],
},
],
isSync: [{
required: true,
message: '',
trigger: ['input', 'blur'],
},
],
})
const elFormRef = ref()
const elSearchFormRef = ref()
// =========== ===========
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
const searchInfo = ref({})
//
const sortChange = ({ prop, order }) => {
const sortMap = {
id: 'id',
}
let sort = sortMap[prop]
if (!sort) {
sort = prop.replace(/[A-Z]/g, match => `_${match.toLowerCase()}`)
}
searchInfo.value.sort = sort
searchInfo.value.order = order
getTableData()
}
//
const onReset = () => {
searchInfo.value = {}
getTableData()
}
//
const onSubmit = () => {
elSearchFormRef.value?.validate(async(valid) => {
if (!valid) return
page.value = 1
pageSize.value = 10
getTableData()
})
}
//
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
//
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
//
const getTableData = async() => {
const table = await getCrawlFieldReplaceList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
// ============== ===============
//
const setOptions = async() => {
}
//
setOptions()
//
const multipleSelection = ref([])
//
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
//
const deleteRow = (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteCrawlFieldReplaceFunc(row)
})
}
//
const onDelete = async() => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
const ids = []
if (multipleSelection.value.length === 0) {
ElMessage({
type: 'warning',
message: '请选择要删除的数据'
})
return
}
multipleSelection.value &&
multipleSelection.value.map(item => {
ids.push(item.id)
})
const res = await deleteCrawlFieldReplaceByIds({ ids })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === ids.length && page.value > 1) {
page.value--
}
getTableData()
}
})
}
//
const type = ref('')
//
const updateCrawlFieldReplaceFunc = async(row) => {
const res = await findCrawlFieldReplace({ id: row.id })
type.value = 'update'
if (res.code === 0) {
formData.value = res.data.recrawlFieldReplace
dialogFormVisible.value = true
}
}
//
const deleteCrawlFieldReplaceFunc = async(row) => {
const res = await deleteCrawlFieldReplace({ id: row.id })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
//
const dialogFormVisible = ref(false)
//
const detailShow = ref(false)
//
const openDetailShow = () => {
detailShow.value = true
}
//
const getDetails = async(row) => {
//
const res = await findCrawlFieldReplace({ id: row.id })
if (res.code === 0) {
formData.value = res.data.recrawlFieldReplace
openDetailShow()
}
}
//
const closeDetailShow = () => {
detailShow.value = false
formData.value = {
fieldName: '',
fieldReplace: '',
fieldSource: '',
id: 0,
}
}
//
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
//
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
fieldName: '',
fieldReplace: '',
fieldSource: '',
id: 0,
}
}
//
const enterDialog = async() => {
elFormRef.value?.validate(async(valid) => {
if (!valid) return
let res
switch (type.value) {
case 'create':
res = await createCrawlFieldReplace(formData.value)
break
case 'update':
res = await updateCrawlFieldReplace(formData.value)
break
default:
res = await createCrawlFieldReplace(formData.value)
break
}
if (res.code === 0) {
ElMessage({
type: 'success',
message: '创建/更改成功'
})
closeDialog()
getTableData()
}
})
}
</script>
<style>
</style>

View File

@ -0,0 +1,128 @@
<template>
<div>
<div class="gva-form-box">
<el-form :model="formData" ref="elFormRef" label-position="right" :rules="rule" label-width="80px">
<el-form-item label="字段名:" prop="fieldName">
<el-input v-model="formData.fieldName" :clearable="true" placeholder="请输入字段名" />
</el-form-item>
<el-form-item label="替换后的值:" prop="fieldReplace">
<el-input v-model="formData.fieldReplace" :clearable="true" placeholder="请输入替换后的值" />
</el-form-item>
<el-form-item label="原始值:" prop="fieldSource">
<el-input v-model="formData.fieldSource" :clearable="true" placeholder="请输入原始值" />
</el-form-item>
<el-form-item label="ID:" prop="id">
<el-input v-model.number="formData.id" :clearable="true" placeholder="请输入" />
</el-form-item>
<el-form-item label="是否同步:" prop="isSync">
<el-select v-model="formData.isSync" placeholder="请选择" style="width:100%" :clearable="true">
<el-option v-for="item in [100]" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="save">保存</el-button>
<el-button type="primary" @click="back">返回</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import {
createCrawlFieldReplace,
updateCrawlFieldReplace,
findCrawlFieldReplace
} from '@/api/crawlFieldReplace'
defineOptions({
name: 'CrawlFieldReplaceForm'
})
//
import { getDictFunc } from '@/utils/format'
import { useRoute, useRouter } from "vue-router"
import { ElMessage } from 'element-plus'
import { ref, reactive } from 'vue'
const route = useRoute()
const router = useRouter()
const type = ref('')
const formData = ref({
fieldName: '',
fieldReplace: '',
fieldSource: '',
id: 0,
})
//
const rule = reactive({
fieldName : [{
required: true,
message: '',
trigger: ['input','blur'],
}],
id : [{
required: true,
message: '',
trigger: ['input','blur'],
}],
isSync : [{
required: true,
message: '',
trigger: ['input','blur'],
}],
})
const elFormRef = ref()
//
const init = async () => {
// urlID find createupdate idurl
if (route.query.id) {
const res = await findCrawlFieldReplace({ ID: route.query.id })
if (res.code === 0) {
formData.value = res.data.recrawlFieldReplace
type.value = 'update'
}
} else {
type.value = 'create'
}
}
init()
//
const save = async() => {
elFormRef.value?.validate( async (valid) => {
if (!valid) return
let res
switch (type.value) {
case 'create':
res = await createCrawlFieldReplace(formData.value)
break
case 'update':
res = await updateCrawlFieldReplace(formData.value)
break
default:
res = await createCrawlFieldReplace(formData.value)
break
}
if (res.code === 0) {
ElMessage({
type: 'success',
message: '创建/更改成功'
})
}
})
}
//
const back = () => {
router.go(-1)
}
</script>
<style>
</style>
@/api/crawlFieldReplace

View File

@ -0,0 +1,138 @@
<template>
<div class="dashboard-line-box">
<div class="dashboard-line-title">
访问趋势
</div>
<div
ref="echart"
class="dashboard-line"
/>
</div>
</template>
<script setup>
import * as echarts from 'echarts'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useWindowResize } from '@/hooks/use-windows-resize'
var dataAxis = []
for (var i = 1; i < 13; i++) {
dataAxis.push(`${i}`)
}
var data = [
220,
182,
191,
234,
290,
330,
310,
123,
442,
321,
90,
149,
]
var yMax = 500
var dataShadow = []
for (let i = 0; i < data.length; i++) {
dataShadow.push(yMax)
}
let chart = null
const echart = ref(null)
useWindowResize(() => {
if (!chart) {
return
}
chart.resize()
})
const initChart = () => {
if (chart) {
chart = null
}
chart = echarts.init(echart.value)
setOptions()
}
const setOptions = () => {
chart.setOption({
grid: {
left: '40',
right: '20',
top: '40',
bottom: '20',
},
xAxis: {
data: dataAxis,
axisTick: {
show: false,
},
axisLine: {
show: false,
},
z: 10,
},
yAxis: {
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
textStyle: {
color: '#999',
},
},
},
dataZoom: [
{
type: 'inside',
},
],
series: [
{
type: 'bar',
barWidth: '40%',
itemStyle: {
borderRadius: [5, 5, 0, 0],
color: '#188df0',
},
emphasis: {
itemStyle: {
color: '#188df0',
},
},
data: data,
},
],
})
}
onMounted(async() => {
await nextTick()
initChart()
})
onUnmounted(() => {
if (!chart) {
return
}
chart.dispose()
chart = null
})
</script>
<style lang="scss" scoped>
.dashboard-line-box {
.dashboard-line {
background-color: #fff;
height: 360px;
width: 100%;
}
.dashboard-line-title {
font-weight: 600;
margin-bottom: 12px;
}
}
</style>

View File

@ -0,0 +1,98 @@
<template>
<div class="commit-table">
<div class="commit-table-title">
更新日志
</div>
<div class="log">
<div
v-for="(item,key) in dataTimeline"
:key="key"
class="log-item"
>
<div class="flex-1 flex key-box">
<span
class="key"
:class="key<3&&'top'"
>{{ key+1 }}</span>
</div>
<div class="flex-5 flex message">{{ item.message }}</div>
<div class="flex-3 flex form">{{ item.from }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
defineOptions({
name: 'DashboardTable',
})
const dataTimeline = ref([])
</script>
<style lang="scss" scoped>
.commit-table{
background-color: #fff;
height: 400px;
&-title{
font-weight: 600;
margin-bottom: 12px;
}
.log{
&-item{
display: flex;
justify-content: space-between;
margin-top: 14px;
.key-box{
justify-content: center;
}
.key{
&.top{
background: #314659;
color: #FFFFFF;;
}
display: inline-flex;
justify-content: center;
align-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: #F0F2F5;
text-align: center;
color:rgba($color: #000000, $alpha: 0.65)
}
.message{
color: rgba(0, 0, 0, 0.65);
}
.form{
color: rgba(0, 0, 0, 0.65);
margin-left: 12px;
}
.flex{
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.flex-1{
flex:1;
}
.flex-2{
flex:2;
}
.flex-3{
flex:3;
}
.flex-4{
flex:4;
}
.flex-5{
flex:5;
}
}
}
}
</style>

View File

@ -0,0 +1,210 @@
<template>
<div class="page">
<div class="gva-card-box">
<div class="gva-card gva-top-card">
<div class="gva-top-card-left">
<div class="gva-top-card-left-title">早安请开始一天的工作吧</div>
<div class="gva-top-card-left-dot">{{ weatherInfo }}</div>
<div>
<!-- <div class="gva-top-card-left-item">
插件仓库
<a
style="color:#409EFF"
target="view_window"
href="https://plugin.gin-vue-admin.com/#/layout/home"
>https://plugin.gin-vue-admin.com</a>
</div> -->
</div>
</div>
<img
src="@/assets/dashboard.png"
class="gva-top-card-right"
alt
>
</div>
</div>
<div class="gva-card-box">
<div class="gva-card quick-entrance">
<div class="gva-card-title">快捷入口</div>
<el-row :gutter="20">
<el-col
v-for="(card, key) in toolCards"
:key="key"
:span="4"
:xs="8"
class="quick-entrance-items"
@click="toTarget(card.name)"
>
<div class="quick-entrance-item">
<div
class="quick-entrance-item-icon"
:style="{ backgroundColor: card.bg }"
>
<el-icon>
<component
:is="card.icon"
:style="{ color: card.color }"
/>
</el-icon>
</div>
<p>{{ card.label }}</p>
</div>
</el-col>
</el-row>
</div>
</div>
<div class="gva-card-box">
<div class="gva-card">
<div class="gva-card-title">数据统计</div>
<div class="p-4">
<el-row :gutter="20">
<el-col
:xs="24"
:sm="18"
>
<echarts-line />
</el-col>
<el-col
:xs="24"
:sm="6"
>
<dashboard-table />
</el-col>
</el-row>
</div>
</div>
</div>
</div>
</template>
<script setup>
import EchartsLine from '@/view/dashboard/dashboardCharts/echartsLine.vue'
import DashboardTable from '@/view/dashboard/dashboardTable/dashboardTable.vue'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useWeatherInfo } from '@/view/dashboard/weather.js'
defineOptions({
name: 'Dashboard'
})
const weatherInfo = useWeatherInfo()
const toolCards = ref([
{
label: '用户管理',
icon: 'monitor',
name: 'user',
color: '#ff9c6e',
bg: 'rgba(255, 156, 110,.3)'
},
{
label: '角色管理',
icon: 'setting',
name: 'authority',
color: '#69c0ff',
bg: 'rgba(105, 192, 255,.3)'
},
{
label: '菜单管理',
icon: 'menu',
name: 'menu',
color: '#b37feb',
bg: 'rgba(179, 127, 235,.3)'
},
{
label: '代码生成器',
icon: 'cpu',
name: 'autoCode',
color: '#ffd666',
bg: 'rgba(255, 214, 102,.3)'
},
{
label: '表单生成器',
icon: 'document-checked',
name: 'formCreate',
color: '#ff85c0',
bg: 'rgba(255, 133, 192,.3)'
},
{
label: '关于我们',
icon: 'user',
name: 'about',
color: '#5cdbd3',
bg: 'rgba(92, 219, 211,.3)'
}
])
const router = useRouter()
const toTarget = (name) => {
router.push({ name })
}
</script>
<style lang="scss" scoped>
.page {
@apply p-0;
.gva-card-box{
@apply p-4;
&+.gva-card-box{
@apply pt-0;
}
}
.gva-card {
@apply box-border bg-white rounded h-auto px-6 py-8 overflow-hidden shadow-sm;
.gva-card-title{
@apply pb-5 border-t-0 border-l-0 border-r-0 border-b border-solid border-gray-100;
}
}
.gva-top-card {
@apply h-72 flex items-center justify-between text-gray-500;
&-left {
@apply h-full flex flex-col w-auto;
&-title {
@apply text-3xl text-gray-600;
}
&-dot {
@apply mt-4 text-gray-600 text-lg;
}
&-item{
+.gva-top-card-left-item{
margin-top: 24px;
}
margin-top: 14px;
}
}
&-right {
height: 600px;
width: 600px;
margin-top: 28px;
}
}
::v-deep(.el-card__header){
@apply p-0 border-gray-200;
}
.card-header{
@apply pb-5 border-b border-solid border-gray-200 border-t-0 border-l-0 border-r-0;
}
.quick-entrance-items {
@apply flex items-center justify-center text-center text-gray-800;
.quick-entrance-item {
@apply px-8 py-6 flex items-center flex-col transition-all duration-100 ease-in-out rounded-lg cursor-pointer;
&:hover{
@apply shadow-lg;
}
&-icon {
@apply flex items-center h-16 w-16 rounded-lg justify-center mx-0 my-auto text-2xl;
}
p {
@apply mt-2.5;
}
}
}
}
.dashboard-icon {
@apply flex items-center text-xl mr-2 text-blue-400;
}
</style>

View File

@ -0,0 +1,35 @@
import axios from 'axios'
import { ref } from 'vue'
const weatherInfo = ref('今日晴0℃ - 10℃天气寒冷注意添加衣物。')
const amapKey = '1f0c8b27920dc41d204800793d629d8e'
export const useWeatherInfo = () => {
ip()
return weatherInfo
}
export const ip = async () => {
// key换成你自己的 https://console.amap.com/dev/index
if (amapKey === '') {
return false
}
const res = await axios.get('https://restapi.amap.com/v3/ip?key=' + amapKey)
var cityCode
if (res.data.status !== '0') {
cityCode = res.data.adcode
} else {
cityCode = '100000'
}
getWeather(cityCode)
}
const getWeather = async (code) => {
const url = 'https://restapi.amap.com/v3/weather/weatherInfo?key=' + amapKey + '&extensions=base&city=' + code
const response = await axios.get(url)
if (response.data.status === '1') {
const s = response.data.lives[0]
weatherInfo.value = s.city + ' 天气:' + s.weather + ' 温度:' + s.temperature + '摄氏度 风向:' + s.winddirection + ' 风力:' + s.windpower + '级 空气湿度:' + s.humidity
}
}

32
src/view/error/index.vue Normal file
View File

@ -0,0 +1,32 @@
<template>
<div>
<div class="w-full h-screen bg-gray-50 flex items-center justify-center">
<div class="flex flex-col items-center text-2xl gap-4">
<img src="../../assets/notFound.png">
<p>页面被神秘力量吸走了如果您是开源版请联系我们修复</p>
<p style="font-size:18px;line-height:40px;">常见问题为当前此角色无当前路由如果确定要使用本路由请到角色管理进行分配</p>
<p></p>
<img
src="../../assets/qm.png"
class="w-16 h-16 mt-20"
>
<el-button @click="toDashboard">返回首页</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { useUserStore } from '@/pinia/modules/user'
import { useRouter } from 'vue-router'
defineOptions({
name: 'Error'
})
const userStore = useUserStore()
const router = useRouter()
const toDashboard = () => {
router.push({ name: userStore.userInfo.authority.defaultRouter })
}
</script>

14
src/view/error/reload.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div />
</template>
<script setup>
import { useRouter } from 'vue-router'
defineOptions({
name: 'Reload'
})
const router = useRouter()
router.go(-1)
</script>

View File

@ -0,0 +1,291 @@
<template>
<div class="break-point">
<div class="gva-table-box">
<el-divider content-position="left">大文件上传</el-divider>
<form
id="fromCont"
method="post"
>
<div
class="fileUpload"
@click="inputChange"
>
选择文件
<input
v-show="false"
id="file"
ref="FileInput"
multiple="multiple"
type="file"
@change="choseFile"
>
</div>
</form>
<el-button
:disabled="limitFileSize"
type="primary"
class="uploadBtn"
@click="getFile"
>上传文件</el-button>
<div class="el-upload__tip">请上传不超过5MB的文件</div>
<div class="list">
<transition
name="list"
tag="p"
>
<div
v-if="file"
class="list-item"
>
<el-icon>
<document />
</el-icon>
<span>{{ file.name }}</span>
<span class="percentage">{{ percentage }}%</span>
<el-progress
:show-text="false"
:text-inside="false"
:stroke-width="2"
:percentage="percentage"
/>
</div>
</transition>
</div>
<div class="tips">此版本为先行体验功能测试版样式美化和性能优化正在进行中上传切片文件和合成的完整文件分别再QMPlusserver目录的breakpointDir文件夹和fileDir文件夹</div>
</div>
</div>
</template>
<script setup>
import SparkMD5 from 'spark-md5'
import {
findFile,
breakpointContinueFinish,
removeChunk,
breakpointContinue
} from '@/api/breakpoint'
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
defineOptions({
name: 'BreakPoint'
})
const file = ref(null)
const fileMd5 = ref('')
const formDataList = ref([])
const waitUpLoad = ref([])
const waitNum = ref(NaN)
const limitFileSize = ref(false)
const percentage = ref(0)
const percentageFlage = ref(true)
//
const choseFile = async(e) => {
const fileR = new FileReader() // reader
const fileInput = e.target.files[0] //
const maxSize = 5 * 1024 * 1024
file.value = fileInput // file 便 func
percentage.value = 0
if (file.value.size < maxSize) {
fileR.readAsArrayBuffer(file.value) // ArrayBuffer
fileR.onload = async e => {
// arrayBuffer e dome e.target.result
const blob = e.target.result
const spark = new SparkMD5.ArrayBuffer() // md5 md5
spark.append(blob) //
fileMd5.value = spark.end() // a md5
const FileSliceCap = 1 * 1024 * 1024 //
let start = 0 //
let end = 0 // a
let i = 0 //
formDataList.value = [] //
while (end < file.value.size) {
// size
start = i * FileSliceCap //
end = (i + 1) * FileSliceCap //
var fileSlice = file.value.slice(start, end) // file.slice h5
const formData = new window.FormData() // FormData
formData.append('fileMd5', fileMd5.value) // Md5
formData.append('file', fileSlice) //
formData.append('chunkNumber', i) //
formData.append('fileName', file.value.name) // formData.appen formData
formDataList.value.push({ key: i, formData }) //
i++
}
const params = {
fileName: file.value.name,
fileMd5: fileMd5.value,
chunkTotal: formDataList.value.length
}
const res = await findFile(params)
//
const finishList = res.data.file.ExaFileChunk //
const IsFinish = res.data.file.IsFinish // md5
if (!IsFinish) {
//
waitUpLoad.value = formDataList.value.filter(all => {
return !(
finishList &&
finishList.some(fi => fi.FileChunkNumber === all.key)
) //
})
} else {
waitUpLoad.value = [] //
ElMessage.success('文件已秒传')
}
waitNum.value = waitUpLoad.value.length //
}
} else {
limitFileSize.value = true
ElMessage('请上传小于5M文件')
}
}
const getFile = () => {
//
if (file.value === null) {
ElMessage('请先上传文件')
return
}
if (percentage.value === 100) {
percentageFlage.value = false
}
sliceFile() //
}
const sliceFile = () => {
waitUpLoad.value &&
waitUpLoad.value.forEach(item => {
//
item.formData.append('chunkTotal', formDataList.value.length) //
const fileR = new FileReader() //
const fileF = item.formData.get('file')
fileR.readAsArrayBuffer(fileF)
fileR.onload = e => {
const spark = new SparkMD5.ArrayBuffer()
spark.append(e.target.result)
item.formData.append('chunkMd5', spark.end()) // md5
upLoadFileSlice(item)
}
})
}
watch(() => waitNum.value, () => { percentage.value = Math.floor(((formDataList.value.length - waitNum.value) / formDataList.value.length) * 100) })
const upLoadFileSlice = async(item) => {
//
const fileRe = await breakpointContinue(item.formData)
if (fileRe.code !== 0) {
return
}
waitNum.value-- //
if (waitNum.value === 0) {
//
const params = {
fileName: file.value.name,
fileMd5: fileMd5.value
}
const res = await breakpointContinueFinish(params)
if (res.code === 0) {
//
const params = {
fileName: file.value.name,
fileMd5: fileMd5.value,
filePath: res.data.filePath,
}
ElMessage.success('上传成功')
await removeChunk(params)
}
}
}
const FileInput = ref(null)
const inputChange = () => {
FileInput.value.dispatchEvent(new MouseEvent('click'))
}
</script>
<style lang='scss' scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
#fromCont{
display: inline-block;
}
.fileUpload{
padding: 3px 10px;
font-size: 12px;
height: 20px;
line-height: 20px;
position: relative;
cursor: pointer;
color: #000;
border: 1px solid #c1c1c1;
border-radius: 4px;
overflow: hidden;
display: inline-block;
input{
position: absolute;
font-size: 100px;
right: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
}
.fileName{
display: inline-block;
vertical-align: top;
margin: 6px 15px 0 15px;
}
.uploadBtn{
position: relative;
top: -10px;
margin-left: 15px;
}
.tips{
margin-top: 30px;
font-size: 14px;
font-weight: 400;
color: #606266;
}
.el-divider{
margin: 0 0 30px 0;
}
.list{
margin-top:15px;
}
.list-item {
display: block;
margin-right: 10px;
color: #606266;
line-height: 25px;
margin-bottom: 5px;
width: 40%;
.percentage{
float: right;
}
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(-30px);
}
</style>

View File

@ -0,0 +1,232 @@
<template>
<div>
<warning-bar title="在资源权限中将此角色的资源权限清空 或者不包含创建者的角色 即可屏蔽此客户资源的显示" />
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button
type="primary"
icon="plus"
@click="openDialog"
>新增</el-button>
</div>
<el-table
ref="multipleTable"
:data="tableData"
style="width: 100%"
tooltip-effect="dark"
row-key="ID"
>
<el-table-column
type="selection"
width="55"
/>
<el-table-column
align="left"
label="接入日期"
width="180"
>
<template #default="scope">
<span>{{ formatDate(scope.row.CreatedAt) }}</span>
</template>
</el-table-column>
<el-table-column
align="left"
label="姓名"
prop="customerName"
width="120"
/>
<el-table-column
align="left"
label="电话"
prop="customerPhoneData"
width="120"
/>
<el-table-column
align="left"
label="接入人ID"
prop="sysUserId"
width="120"
/>
<el-table-column
align="left"
label="操作"
min-width="160"
>
<template #default="scope">
<el-button
type="primary"
link
icon="edit"
@click="updateCustomer(scope.row)"
>变更</el-button>
<el-button
type="primary"
link
icon="delete"
@click="deleteCustomer(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-dialog
v-model="dialogFormVisible"
:before-close="closeDialog"
title="客户"
>
<el-form
:inline="true"
:model="form"
label-width="80px"
>
<el-form-item label="客户名">
<el-input
v-model="form.customerName"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="客户电话">
<el-input
v-model="form.customerPhoneData"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDialog"> </el-button>
<el-button
type="primary"
@click="enterDialog"
> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {
createExaCustomer,
updateExaCustomer,
deleteExaCustomer,
getExaCustomer,
getExaCustomerList
} from '@/api/customer'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { formatDate } from '@/utils/format'
defineOptions({
name: 'Customer'
})
const form = ref({
customerName: '',
customerPhoneData: ''
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
//
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
//
const getTableData = async() => {
const table = await getExaCustomerList({ page: page.value, pageSize: pageSize.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const dialogFormVisible = ref(false)
const type = ref('')
const updateCustomer = async(row) => {
const res = await getExaCustomer({ ID: row.ID })
type.value = 'update'
if (res.code === 0) {
form.value = res.data.customer
dialogFormVisible.value = true
}
}
const closeDialog = () => {
dialogFormVisible.value = false
form.value = {
customerName: '',
customerPhoneData: ''
}
}
const deleteCustomer = async(row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
const res = await deleteExaCustomer({ ID: row.ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
})
}
const enterDialog = async() => {
let res
switch (type.value) {
case 'create':
res = await createExaCustomer(form.value)
break
case 'update':
res = await updateExaCustomer(form.value)
break
default:
res = await createExaCustomer(form.value)
break
}
if (res.code === 0) {
closeDialog()
getTableData()
}
}
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
</script>
<style></style>

View File

@ -0,0 +1,22 @@
<template>
<div>
<router-view v-slot="{ Component }">
<transition
mode="out-in"
name="el-fade-in-linear"
>
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script setup>
import { useRouterStore } from '@/pinia/modules/router'
const routerStore = useRouterStore()
defineOptions({
name: 'Example'
})
</script>

View File

@ -0,0 +1,247 @@
<template>
<div v-loading.fullscreen.lock="fullscreenLoading">
<div class="gva-table-box">
<warning-bar
title="点击“文件名/备注”可以编辑文件名或者备注内容。"
/>
<div class="gva-btn-list">
<upload-common
:image-common="imageCommon"
@on-success="getTableData"
/>
<upload-image
:image-url="imageUrl"
:file-size="512"
:max-w-h="1080"
@on-success="getTableData"
/>
<el-input
v-model="search.keyword"
class="keyword"
placeholder="请输入文件名或备注"
/>
<el-button
type="primary"
icon="search"
@click="getTableData"
>查询</el-button>
</div>
<el-table :data="tableData">
<el-table-column
align="left"
label="预览"
width="100"
>
<template #default="scope">
<CustomPic
pic-type="file"
:pic-src="scope.row.url"
preview
/>
</template>
</el-table-column>
<el-table-column
align="left"
label="日期"
prop="UpdatedAt"
width="180"
>
<template #default="scope">
<div>{{ formatDate(scope.row.UpdatedAt) }}</div>
</template>
</el-table-column>
<el-table-column
align="left"
label="文件名/备注"
prop="name"
width="180"
>
<template #default="scope">
<div
class="name"
@click="editFileNameFunc(scope.row)"
>{{ scope.row.name }}</div>
</template>
</el-table-column>
<el-table-column
align="left"
label="链接"
prop="url"
min-width="300"
/>
<el-table-column
align="left"
label="标签"
prop="tag"
width="100"
>
<template #default="scope">
<el-tag
:type="scope.row.tag === 'jpg' ? 'info' : 'success'"
disable-transitions
>{{ scope.row.tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column
align="left"
label="操作"
width="160"
>
<template #default="scope">
<el-button
icon="download"
type="primary"
link
@click="downloadFile(scope.row)"
>下载</el-button>
<el-button
icon="delete"
type="primary"
link
@click="deleteFileFunc(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:style="{ float: 'right', padding: '20px' }"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import { getFileList, deleteFile, editFileName } from '@/api/fileUploadAndDownload'
import { downloadImage } from '@/utils/downloadImg'
import CustomPic from '@/components/customPic/index.vue'
import UploadImage from '@/components/upload/image.vue'
import UploadCommon from '@/components/upload/common.vue'
import { formatDate } from '@/utils/format'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'Upload',
})
const path = ref(import.meta.env.VITE_BASE_API)
const imageUrl = ref('')
const imageCommon = ref('')
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const search = ref({})
const tableData = ref([])
//
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
//
const getTableData = async() => {
const table = await getFileList({ page: page.value, pageSize: pageSize.value, ...search.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const deleteFileFunc = async(row) => {
ElMessageBox.confirm('此操作将永久删除文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async() => {
const res = await deleteFile(row)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功!',
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '已取消删除',
})
})
}
const downloadFile = (row) => {
if (row.url.indexOf('http://') > -1 || row.url.indexOf('https://') > -1) {
downloadImage(row.url, row.name)
} else {
debugger
downloadImage(path.value + '/' + row.url, row.name)
}
}
/**
* 编辑文件名或者备注
* @param row
* @returns {Promise<void>}
*/
const editFileNameFunc = async(row) => {
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '不能为空',
inputValue: row.name
}).then(async({ value }) => {
row.name = value
// console.log(row)
const res = await editFileName(row)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '编辑成功!',
})
getTableData()
}
}).catch(() => {
ElMessage({
type: 'info',
message: '取消修改'
})
})
}
</script>
<style scoped>
.name {
cursor: pointer;
}
</style>

361
src/view/init/index.vue Normal file
View File

@ -0,0 +1,361 @@
<template>
<div class="rounded-lg flex items-center justify-evenly w-full h-full relative bg-white md:w-screen md:h-screen md:bg-[#194bfb] overflow-hidden">
<div class="rounded-md w-full h-full flex items-center justify-center overflow-hidden">
<div class="oblique h-[130%] w-3/5 bg-white transform -rotate-12 absolute -ml-80" />
<div
v-if="!page.showForm"
:class="[page.showReadme ?'slide-out-right' :'slide-in-fwd-top' ]"
>
<div class=" text-lg">
<div class="font-sans text-4xl font-bold text-center mb-4">WEB-ADMIN</div>
<p class="text-gray-600 mb-2">初始化须知</p>
<p class="text-gray-600 mb-2">1.您需有用一定的VUE和GOLANG基础</p>
<p class="text-gray-600 mb-2">2.请您确认是否已经阅读过官方文档初始化视频</p>
<p class="text-gray-600 mb-2">3.请您确认是否了解后续的配置流程</p>
<p class="text-gray-600 mb-2">4.如果您使用mysql数据库请确认数据库引擎为<span class="text-red-600 font-bold text-3xl ml-2 ">innoDB</span></p>
<p class="text-gray-600 mb-2">开发组不为文档中书写过的内容提供无偿服务</p>
<p class="flex items-center justify-between mt-8">
<el-button
type="primary"
size="large"
@click="showNext"
>
我已确认
</el-button>
</p>
</div>
</div>
<div
v-if="page.showForm "
:class="[ page.showForm ? 'slide-in-left' : 'slide-out-right' ]"
class="w-96"
>
<el-form
ref="formRef"
:model="form"
label-width="100px"
size="large"
>
<el-form-item label="数据库类型">
<el-select
v-model="form.dbType"
placeholder="请选择"
class="w-full"
@change="changeDB"
>
<el-option
key="mysql"
label="mysql"
value="mysql"
/>
<el-option
key="pgsql"
label="pgsql"
value="pgsql"
/>
<el-option
key="oracle"
label="oracle"
value="oracle"
/>
<el-option
key="mssql"
label="mssql"
value="mssql"
/>
<el-option
key="sqlite"
label="sqlite"
value="sqlite"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="form.dbType !== 'sqlite'"
label="host"
>
<el-input
v-model="form.host"
placeholder="请输入数据库链接"
/>
</el-form-item>
<el-form-item
v-if="form.dbType !== 'sqlite'"
label="port"
>
<el-input
v-model="form.port"
placeholder="请输入数据库端口"
/>
</el-form-item>
<el-form-item
v-if="form.dbType !== 'sqlite'"
label="userName"
>
<el-input
v-model="form.userName"
placeholder="请输入数据库用户名"
/>
</el-form-item>
<el-form-item
v-if="form.dbType !== 'sqlite'"
label="password"
>
<el-input
v-model="form.password"
placeholder="请输入数据库密码(没有则为空)"
/>
</el-form-item>
<el-form-item label="dbName">
<el-input
v-model="form.dbName"
placeholder="请输入数据库名称"
/>
</el-form-item>
<el-form-item
v-if="form.dbType === 'sqlite'"
label="dbPath"
>
<el-input
v-model="form.dbPath"
placeholder="请输入sqlite数据库文件存放路径"
/>
</el-form-item>
<el-form-item>
<div style="text-align: right">
<el-button
type="primary"
@click="onSubmit"
>立即初始化</el-button>
</div>
</el-form-item>
</el-form>
</div>
</div>
<div class="hidden md:block w-1/2 h-full float-right bg-[#194bfb]"><img
class="h-full"
src="@/assets/login_right_banner.jpg"
alt="banner"
></div>
</div>
</template>
<script setup>
// @ts-ignore
import { initDB } from '@/api/initdb'
import { reactive, ref } from 'vue'
import { ElLoading, ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
defineOptions({
name: 'Init',
})
const router = useRouter()
const page = reactive({
showReadme: false,
showForm: false
})
const showNext = () => {
page.showReadme = false
setTimeout(() => {
page.showForm = true
}, 20)
}
const goDoc = () => {
// test
}
const out = ref(false)
const form = reactive({
dbType: 'mysql',
host: '127.0.0.1',
port: '3306',
userName: 'root',
password: '',
dbName: 'gva',
dbPath: ''
})
const changeDB = (val) => {
switch (val) {
case 'mysql':
Object.assign(form, {
dbType: 'mysql',
host: '127.0.0.1',
port: '3306',
userName: 'root',
password: '',
dbName: 'gva',
dbPath: ''
})
break
case 'pgsql':
Object.assign(form, {
dbType: 'pgsql',
host: '127.0.0.1',
port: '5432',
userName: 'postgres',
password: '',
dbName: 'gva',
dbPath: ''
})
break
case 'oracle':
Object.assign(form, {
dbType: 'oracle',
host: '127.0.0.1',
port: '1521',
userName: 'oracle',
password: '',
dbName: 'gva',
dbPath: ''
})
break
case 'mssql':
Object.assign(form, {
dbType: 'mssql',
host: '127.0.0.1',
port: '1433',
userName: 'mssql',
password: '',
dbName: 'gva',
dbPath: ''
})
break
case 'sqlite':
Object.assign(form, {
dbType: 'sqlite',
host: '',
port: '',
userName: '',
password: '',
dbName: 'gva',
dbPath: ''
})
break
default:
Object.assign(form, {
dbType: 'mysql',
host: '127.0.0.1',
port: '3306',
userName: 'root',
password: '',
dbName: 'gva',
dbPath: ''
})
}
}
const onSubmit = async() => {
const loading = ElLoading.service({
lock: true,
text: '正在初始化数据库,请稍候',
spinner: 'loading',
background: 'rgba(0, 0, 0, 0.7)',
})
try {
const res = await initDB(form)
if (res.code === 0) {
out.value = true
ElMessage({
type: 'success',
message: res.msg,
})
router.push({ name: 'Login' })
}
loading.close()
} catch (err) {
loading.close()
}
}
</script>
<style lang="scss" scoped>
.slide-in-fwd-top {
-webkit-animation: slide-in-fwd-top 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)
both;
animation: slide-in-fwd-top 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.slide-out-right {
-webkit-animation: slide-out-right 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53)
both;
animation: slide-out-right 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
}
.slide-in-left {
-webkit-animation: slide-in-left 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)
both;
animation: slide-in-left 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
@-webkit-keyframes slide-in-fwd-top {
0% {
transform: translateZ(-1400px) translateY(-800px);
opacity: 0;
}
100% {
transform: translateZ(0) translateY(0);
opacity: 1;
}
}
@keyframes slide-in-fwd-top {
0% {
transform: translateZ(-1400px) translateY(-800px);
opacity: 0;
}
100% {
transform: translateZ(0) translateY(0);
opacity: 1;
}
}
@-webkit-keyframes slide-out-right {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(1000px);
opacity: 0;
}
}
@keyframes slide-out-right {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(1000px);
opacity: 0;
}
}
@-webkit-keyframes slide-in-left {
0% {
transform: translateX(-1000px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slide-in-left {
0% {
transform: translateX(-1000px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@media (max-width: 750px) {
.form {
width: 94vw !important;
padding: 0;
}
}
</style>

View File

@ -0,0 +1,149 @@
<template>
<div :style="{ background: userStore.sideMode }">
<el-scrollbar style="height: calc(100vh - 60px)">
<transition
:duration="{ enter: 800, leave: 100 }"
mode="out-in"
name="el-fade-in-linear"
>
<el-menu
:collapse="isCollapse"
:collapse-transition="false"
:default-active="active"
:background-color="theme.background"
:active-text-color="theme.active"
class="el-menu-vertical"
unique-opened
@select="selectMenuItem"
>
<template v-for="item in routerStore.asyncRouters[0].children">
<aside-component
v-if="!item.hidden"
:key="item.name"
:is-collapse="isCollapse"
:router-info="item"
:theme="theme"
/>
</template>
</el-menu>
</transition>
</el-scrollbar>
</div>
</template>
<script setup>
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
import { emitter } from '@/utils/bus.js'
import { ref, watch, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/pinia/modules/user'
import { useRouterStore } from '@/pinia/modules/router'
defineOptions({
name: 'Aside',
})
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const routerStore = useRouterStore()
const theme = ref({})
const getTheme = () => {
switch (userStore.sideMode) {
case '#fff':
theme.value = {
background: '#fff',
activeBackground: 'var(--el-color-primary)',
activeText: '#fff',
normalText: '#333',
hoverBackground: 'rgba(64, 158, 255, 0.08)',
hoverText: '#333',
}
break
case '#191a23':
theme.value = {
background: '#191a23',
activeBackground: 'var(--el-color-primary)',
activeText: '#fff',
normalText: '#fff',
hoverBackground: 'rgba(64, 158, 255, 0.08)',
hoverText: '#fff',
}
break
}
}
getTheme()
const active = ref('')
watch(() => route, () => {
active.value = route.meta.activeName || route.name
}, { deep: true })
watch(() => userStore.sideMode, () => {
getTheme()
})
const isCollapse = ref(false)
const initPage = () => {
active.value = route.meta.activeName || route.name
const screenWidth = document.body.clientWidth
if (screenWidth < 1000) {
isCollapse.value = !isCollapse.value
}
emitter.on('collapse', (item) => {
isCollapse.value = item
})
}
initPage()
onUnmounted(() => {
emitter.off('collapse')
})
const selectMenuItem = (index, _, ele, aaa) => {
const query = {}
const params = {}
routerStore.routeMap[index]?.parameters &&
routerStore.routeMap[index]?.parameters.forEach((item) => {
if (item.type === 'query') {
query[item.key] = item.value
} else {
params[item.key] = item.value
}
})
if (index === route.name) return
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
window.open(index)
} else {
router.push({ name: index, query, params })
}
}
</script>
<style lang="scss">
.el-sub-menu__title:hover,
.el-menu-item:hover {
background: transparent;
}
.el-scrollbar {
.el-scrollbar__view {
height: 100%;
}
}
.menu-info {
.menu-contorl {
line-height: 52px;
font-size: 20px;
display: table-cell;
vertical-align: middle;
}
}
</style>

374
src/view/layout/index.vue Normal file
View File

@ -0,0 +1,374 @@
<template>
<el-container class="layout-cont">
<el-container :class="[isSider?'openside':'hideside',isMobile ? 'mobile': '']">
<el-row
:class="[isShadowBg && isMobile?'bg-black opacity-30 w-full h-full absolute top-0 left-0 z-[1001]':'']"
@click="changeShadow()"
/>
<el-aside
class="main-cont gva-aside"
:style="{width:asideWidth()}"
>
<div
class="min-h-[60px] text-center transition-all duration-300 flex items-center justify-center gap-2"
:style="{background: backgroundColor}"
>
<img
alt
class="w-9 h-9 p-1 bg-white rounded-full"
:src="$GIN_VUE_ADMIN.appLogo"
>
<div
v-if="isSider"
class="inline-flex font-bold text-2xl"
:style="{color:textColor}"
>{{ $GIN_VUE_ADMIN.appName }}</div>
</div>
<Aside class="aside" />
</el-aside>
<!-- 分块滑动功能 -->
<el-main class="main-cont main-right">
<transition
:duration="{ enter: 800, leave: 100 }"
mode="out-in"
name="el-fade-in-linear"
>
<div
:style="{width: `calc(100% - ${getAsideWidth()})`}"
class="fixed top-0 box-border z-50"
>
<el-row>
<el-col>
<el-header class="header-cont">
<el-row class="p-0 h-full">
<el-col
:xs="2"
:lg="1"
:md="1"
:sm="1"
:xl="1"
class="z-50 flex items-center pl-3"
>
<div
class="text-black cursor-pointer text-lg leading-5"
@click="totalCollapse"
>
<div
v-if="isCollapse"
class="gvaIcon gvaIcon-arrow-double-right"
/>
<div
v-else
class="gvaIcon gvaIcon-arrow-double-left"
/>
</div>
</el-col>
<el-col
:xs="10"
:lg="14"
:md="14"
:sm="9"
:xl="14"
:pull="1"
class="flex items-center"
>
<!-- 修改为手机端不显示顶部标签 -->
<el-breadcrumb
v-show="!isMobile"
class="breadcrumb"
>
<el-breadcrumb-item
v-for="item in matched.slice(1,matched.length)"
:key="item.path"
>{{ fmtTitle(item.meta.title,route) }}</el-breadcrumb-item>
</el-breadcrumb>
</el-col>
<el-col
:xs="12"
:lg="9"
:md="9"
:sm="14"
:xl="9"
class="flex items-center justify-end"
>
<div class="mr-1.5 flex items-center">
<Search />
<el-dropdown>
<div class="flex justify-center items-center h-full w-full">
<span class="cursor-pointer flex justify-center items-center">
<CustomPic />
<span
v-show="!isMobile"
style="margin-left: 5px"
>{{ userStore.userInfo.nickName }}</span>
<el-icon>
<arrow-down />
</el-icon>
</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<span class="font-bold">
当前角色{{ userStore.userInfo.authority.authorityName }}
</span>
</el-dropdown-item>
<template v-if="userStore.userInfo.authorities">
<el-dropdown-item
v-for="item in userStore.userInfo.authorities.filter(i=>i.authorityId!==userStore.userInfo.authorityId)"
:key="item.authorityId"
@click="changeUserAuth(item.authorityId)"
>
<span>
切换为{{ item.authorityName }}
</span>
</el-dropdown-item>
</template>
<el-dropdown-item icon="avatar">
<div
class="command-box"
style="display: flex"
@click="handleCommand"
>
<div>指令菜单</div>
<div style="margin-left: 8px">
<span class="button">{{ first }}</span>
+
<span class="button">K</span>
</div>
</div>
</el-dropdown-item>
<el-dropdown-item
icon="avatar"
@click="toPerson"
>个人信息</el-dropdown-item>
<el-dropdown-item
icon="reading-lamp"
@click="userStore.LoginOut"
> </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-col>
</el-row>
</el-header>
</el-col>
</el-row>
<!-- 当前面包屑用路由自动生成可根据需求修改 -->
<!--
:to="{ path: item.path }" 暂时注释不用-->
<HistoryComponent ref="layoutHistoryComponent" />
</div>
</transition>
<router-view
v-if="reloadFlag"
v-slot="{ Component }"
class="admin-box"
>
<div
id="gva-base-load-dom"
>
<transition
mode="out-in"
name="el-fade-in-linear"
>
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</div>
</router-view>
<BottomInfo />
<setting />
<CommandMenu ref="command" />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import Aside from '@/view/layout/aside/index.vue'
import HistoryComponent from '@/view/layout/aside/historyComponent/history.vue'
import Search from '@/view/layout/search/search.vue'
import BottomInfo from '@/view/layout/bottomInfo/bottomInfo.vue'
import CustomPic from '@/components/customPic/index.vue'
import CommandMenu from '@/components/commandMenu/index.vue'
import Setting from './setting/index.vue'
import { setUserAuthority } from '@/api/user'
import { emitter } from '@/utils/bus.js'
import { computed, ref, onMounted, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useRouterStore } from '@/pinia/modules/router'
import { fmtTitle } from '@/utils/fmtRouterTitle'
import { useUserStore } from '@/pinia/modules/user'
defineOptions({
name: 'Layout',
})
const router = useRouter()
const route = useRoute()
const routerStore = useRouterStore()
//
const isCollapse = ref(false)
const isSider = ref(true)
const isMobile = ref(false)
const first = ref('')
const initPage = () => {
//
if (window.localStorage.getItem('osType') === 'WIN') {
first.value = 'Ctrl'
} else {
first.value = '⌘'
}
// ctrlk
const handleKeyDown = (e) => {
if (e.ctrlKey && e.key === 'k') {
//
e.preventDefault()
handleCommand()
}
}
window.addEventListener('keydown', handleKeyDown)
const screenWidth = document.body.clientWidth
if (screenWidth < 1000) {
isMobile.value = true
isSider.value = false
isCollapse.value = true
} else if (screenWidth >= 1000 && screenWidth < 1200) {
isMobile.value = false
isSider.value = false
isCollapse.value = true
} else {
isMobile.value = false
isSider.value = true
isCollapse.value = false
}
}
initPage()
const command = ref()
const handleCommand = () => {
command.value.open()
}
onMounted(() => {
//
emitter.emit('collapse', isCollapse.value)
emitter.emit('mobile', isMobile.value)
emitter.on('reload', reload)
window.onresize = () => {
return (() => {
initPage()
emitter.emit('collapse', isCollapse.value)
emitter.emit('mobile', isMobile.value)
})()
}
if (userStore.loadingInstance) {
userStore.loadingInstance.close()
}
})
const userStore = useUserStore()
const asideWidth = () => {
if (isMobile.value) {
return isCollapse.value ? '0px' : '220px'
}
return isCollapse.value ? '54px' : '220px'
}
const getAsideWidth = () => {
if (isMobile.value) return '0px'
return isCollapse.value ? '54px' : '220px'
}
const textColor = computed(() => {
if (userStore.sideMode === 'dark') {
return '#fff'
} else if (userStore.sideMode === 'light') {
return '#191a23'
} else {
return userStore.baseColor
}
})
const backgroundColor = computed(() => {
if (userStore.sideMode === 'dark') {
return '#191a23'
} else if (userStore.sideMode === 'light') {
return '#fff'
} else {
return userStore.sideMode
}
})
const matched = computed(() => route.meta.matched)
const changeUserAuth = async(id) => {
const res = await setUserAuthority({
authorityId: id
})
if (res.code === 0) {
window.sessionStorage.setItem('needCloseAll', 'true')
window.location.reload()
}
}
const reloadFlag = ref(true)
let reloadTimer = null
const reload = async() => {
if (reloadTimer) {
window.clearTimeout(reloadTimer)
}
reloadTimer = window.setTimeout(async() => {
if (route.meta.keepAlive) {
reloadFlag.value = false
await nextTick()
reloadFlag.value = true
} else {
const title = route.meta.title
router.push({ name: 'Reload', params: { title }})
}
}, 400)
}
const isShadowBg = ref(false)
const totalCollapse = () => {
isCollapse.value = !isCollapse.value
isSider.value = !isCollapse.value
isShadowBg.value = !isCollapse.value
emitter.emit('collapse', isCollapse.value)
}
const toPerson = () => {
router.push({ name: 'person' })
}
const changeShadow = () => {
isShadowBg.value = !isShadowBg.value
isSider.value = !!isCollapse.value
totalCollapse()
}
</script>
<style lang="scss">
.button {
font-size: 12px;
color: #666;
background: rgb(250,250,250);
width: 25px!important;
padding: 4px 8px !important;
border: 1px solid #eaeaea;
margin-right: 4px;
border-radius: 4px;
}
:deep .el-overlay {
background-color: hsla(0,0%,100%,.9) !important;
}
</style>

25
src/view/routerHolder.vue Normal file
View File

@ -0,0 +1,25 @@
<!-- 此路由可作为父类路由通用路由页面使用 如需自定义父类路由页面 请参考 @/view/superAdmin/index.vue -->
<template>
<div>
<router-view v-slot="{ Component }">
<transition
mode="out-in"
name="el-fade-in-linear"
>
<keep-alive :include="routerStore.keepAliveRouters">
<div>
<component :is="Component" />
</div>
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script setup>
defineOptions({
name: 'RouterHolder'
})
import { useRouterStore } from '@/pinia/modules/router'
const routerStore = useRouterStore()
</script>