feat(other): init
This commit is contained in:
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
12
src/App.vue
Normal file
12
src/App.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AppProvider>
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component" />
|
||||
</router-view>
|
||||
</AppProvider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AppProvider from '@/components/common/AppProvider.vue'
|
||||
// App
|
||||
</script>
|
||||
13
src/api/index.js
Normal file
13
src/api/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getUser: () => request.get('/user'),
|
||||
refreshToken: () => request.post('/auth/refreshToken', null, { noNeedTip: true }),
|
||||
// 上传
|
||||
uploadImg: (data) =>
|
||||
request.post('/upload', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}),
|
||||
}
|
||||
BIN
src/assets/images/404.webp
Normal file
BIN
src/assets/images/404.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/images/login_banner.webp
Normal file
BIN
src/assets/images/login_banner.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/images/login_bg.webp
Normal file
BIN
src/assets/images/login_bg.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/assets/images/logo.png
Normal file
BIN
src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
1
src/assets/svg/logo.svg
Normal file
1
src/assets/svg/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-92.2 312.9c-63.4 0-85.4-28.6-97.1-64.1c-16.3-51-21.5-84.3-63-84.3c-22.4 0-45.1 16.1-45.1 61.2c0 35.2 18 57.2 43.3 57.2c28.6 0 47.6-21.3 47.6-21.3l11.7 31.9s-19.8 19.4-61.2 19.4c-51.3 0-79.9-30.1-79.9-85.8c0-57.9 28.6-92 82.5-92c73.5 0 80.8 41.4 100.8 101.9c8.8 26.8 24.2 46.2 61.2 46.2c24.9 0 38.1-5.5 38.1-19.1c0-19.9-21.8-22-49.9-28.6c-30.4-7.3-42.5-23.1-42.5-48c0-40 32.3-52.4 65.2-52.4c37.4 0 60.1 13.6 63 46.6l-36.7 4.4c-1.5-15.8-11-22.4-28.6-22.4c-16.1 0-26 7.3-26 19.8c0 11 4.8 17.6 20.9 21.3c32.7 7.1 71.8 12 71.8 57.5c.1 36.7-30.7 50.6-76.1 50.6z"></path></svg>
|
||||
|
After Width: | Height: | Size: 811 B |
51
src/components/Echarts.vue
Normal file
51
src/components/Echarts.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<v-chart :loading="loading" class="chart" :option="option" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { use } from 'echarts/core'
|
||||
import { BarChart, PieChart } from 'echarts/charts'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
} from 'echarts/components'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
|
||||
use([
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
BarChart,
|
||||
CanvasRenderer,
|
||||
PieChart,
|
||||
DataZoomComponent,
|
||||
])
|
||||
|
||||
import VChart, { THEME_KEY } from 'vue-echarts'
|
||||
|
||||
import { provide } from 'vue'
|
||||
|
||||
provide(THEME_KEY, 'white')
|
||||
|
||||
defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
97
src/components/Editor.vue
Normal file
97
src/components/Editor.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div ref="_ref" style="border: 1px solid #ccc">
|
||||
<Toolbar
|
||||
style="border-bottom: 1px solid #ccc"
|
||||
:editor="editorRef"
|
||||
:default-config="toolbarConfig"
|
||||
mode="default"
|
||||
/>
|
||||
<Editor
|
||||
v-model="htmlValue"
|
||||
:style="{ height: height + 'px', overflowY: 'hidden' }"
|
||||
:default-config="editorConfig"
|
||||
mode="default"
|
||||
@on-created="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import '@wangeditor/editor/dist/css/style.css' // 引入 css
|
||||
|
||||
import { onBeforeUnmount, shallowRef, ref } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import api from '@/api'
|
||||
|
||||
const props = defineProps({
|
||||
valueHtml: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:valueHtml'])
|
||||
|
||||
const editorRef = shallowRef()
|
||||
|
||||
const _ref = ref(null)
|
||||
|
||||
const toolbarConfig = {}
|
||||
|
||||
const editorConfig = { placeholder: '请输入游戏介绍...', MENU_CONF: {} }
|
||||
|
||||
const htmlValue = computed({
|
||||
get() {
|
||||
return props.valueHtml
|
||||
},
|
||||
set(value) {
|
||||
emit('update:valueHtml', value)
|
||||
},
|
||||
})
|
||||
|
||||
editorConfig.MENU_CONF['uploadImage'] = {
|
||||
allowedFileTypes: ['image/*'],
|
||||
base64LimitSize: 5 * 1024, // 5kb
|
||||
// 超时时间,默认为 10 秒
|
||||
timeout: 5 * 1000, // 5 秒
|
||||
|
||||
async customUpload(file, insertFn) {
|
||||
await upFile(file, insertFn)
|
||||
},
|
||||
}
|
||||
|
||||
editorConfig.MENU_CONF['uploadVideo'] = {
|
||||
allowedFileTypes: ['video/*'],
|
||||
|
||||
async customUpload(file, insertFn) {
|
||||
await upFile(file, insertFn)
|
||||
},
|
||||
}
|
||||
|
||||
// 组件销毁时,也及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
const handleCreated = (editor) => {
|
||||
editorRef.value = editor // 记录 editor 实例,重要!
|
||||
editor.on('fullScreen', () => {
|
||||
_ref.value.style.zIndex = 10
|
||||
})
|
||||
}
|
||||
|
||||
const upFile = async (file, insertFn) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const res = await api.uploadImg(formData)
|
||||
insertFn(res.data.data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
62
src/components/Upload.vue
Normal file
62
src/components/Upload.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<n-upload
|
||||
v-model:file-list="List"
|
||||
:max="max"
|
||||
list-type="image-card"
|
||||
:custom-request="customRequest"
|
||||
@before-upload="beforeUpload"
|
||||
>
|
||||
点击上传
|
||||
</n-upload>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from '@/api'
|
||||
|
||||
const prop = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:list'])
|
||||
|
||||
const List = computed({
|
||||
get() {
|
||||
return prop.list
|
||||
},
|
||||
set(val) {
|
||||
emit('update:list', val)
|
||||
},
|
||||
})
|
||||
|
||||
// 图片上传限制
|
||||
const beforeUpload = ({ file }) => {
|
||||
if (!(file.file?.type === 'image/png' || file.file?.type === 'image/jpeg')) {
|
||||
$message.error('只能上传png或jpg格式的图片文件,请重新上传')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const customRequest = async ({ file, onFinish, onError }) => {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
const res = await api.uploadImg(formData)
|
||||
$message.success(res.msg)
|
||||
file.url = res.data.data
|
||||
onFinish()
|
||||
} catch (error) {
|
||||
onError()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
23
src/components/common/AppFooter.vue
Normal file
23
src/components/common/AppFooter.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<footer f-c-c flex-col text-14 color="#6a6a6a">
|
||||
<p>
|
||||
Copyright © 2022-present
|
||||
<a
|
||||
href="https://github.com/zclzone"
|
||||
target="__blank"
|
||||
hover="decoration-underline color-primary"
|
||||
>
|
||||
Ronnie Zhang
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="http://beian.miit.gov.cn/"
|
||||
target="__blank"
|
||||
hover="decoration-underline color-primary"
|
||||
>
|
||||
赣ICP备2020015008号-1
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</template>
|
||||
30
src/components/common/AppProvider.vue
Normal file
30
src/components/common/AppProvider.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<n-config-provider
|
||||
wh-full
|
||||
:locale="zhCN"
|
||||
:date-locale="dateZhCN"
|
||||
:theme="appStore.isDark ? darkTheme : undefined"
|
||||
:theme-overrides="naiveThemeOverrides"
|
||||
>
|
||||
<slot />
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { zhCN, dateZhCN, darkTheme } from 'naive-ui'
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
import { kebabCase } from 'lodash-es'
|
||||
import { naiveThemeOverrides } from '~/settings'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
function setupCssVar() {
|
||||
const common = naiveThemeOverrides.common
|
||||
for (const key in common) {
|
||||
useCssVar(`--${kebabCase(key)}`, document.documentElement).value = common[key] || ''
|
||||
if (key === 'primaryColor') window.localStorage.setItem('__THEME_COLOR__', common[key] || '')
|
||||
}
|
||||
}
|
||||
setupCssVar()
|
||||
</script>
|
||||
162
src/components/common/ScrollX.vue
Normal file
162
src/components/common/ScrollX.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="wrapper" @mousewheel.prevent="handleMouseWheel">
|
||||
<template v-if="showArrow && isOverflow">
|
||||
<div class="left dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: 120 })">
|
||||
<icon-ic:baseline-keyboard-arrow-left />
|
||||
</div>
|
||||
<div class="right dark:bg-dark!" @click="handleMouseWheel({ wheelDelta: -120 })">
|
||||
<icon-ic:baseline-keyboard-arrow-right />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
ref="content"
|
||||
class="content"
|
||||
:class="{ overflow: isOverflow && showArrow }"
|
||||
:style="{
|
||||
transform: `translateX(${translateX}px)`,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { debounce, useResize } from '@/utils'
|
||||
|
||||
defineProps({
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const translateX = ref(0)
|
||||
const content = ref(null)
|
||||
const wrapper = ref(null)
|
||||
const isOverflow = ref(false)
|
||||
|
||||
const refreshIsOverflow = debounce(() => {
|
||||
const wrapperWidth = wrapper.value?.offsetWidth
|
||||
const contentWidth = content.value?.offsetWidth
|
||||
isOverflow.value = contentWidth > wrapperWidth
|
||||
resetTranslateX(wrapperWidth, contentWidth)
|
||||
}, 200)
|
||||
|
||||
function handleMouseWheel(e) {
|
||||
const { wheelDelta } = e
|
||||
const wrapperWidth = wrapper.value?.offsetWidth
|
||||
const contentWidth = content.value?.offsetWidth
|
||||
/**
|
||||
* @wheelDelta 平行滚动的值 >0: 右移 <0: 左移
|
||||
* @translateX 内容translateX的值
|
||||
* @wrapperWidth 容器的宽度
|
||||
* @contentWidth 内容的宽度
|
||||
*/
|
||||
if (wheelDelta < 0) {
|
||||
if (wrapperWidth > contentWidth && translateX.value < -10) return
|
||||
if (wrapperWidth <= contentWidth && contentWidth + translateX.value - wrapperWidth < -10) return
|
||||
}
|
||||
if (wheelDelta > 0 && translateX.value > 10) {
|
||||
return
|
||||
}
|
||||
|
||||
translateX.value += wheelDelta
|
||||
resetTranslateX(wrapperWidth, contentWidth)
|
||||
}
|
||||
|
||||
const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
|
||||
if (!isOverflow.value) {
|
||||
translateX.value = 0
|
||||
} else if (-translateX.value > contentWidth - wrapperWidth) {
|
||||
translateX.value = wrapperWidth - contentWidth
|
||||
} else if (translateX.value > 0) {
|
||||
translateX.value = 0
|
||||
}
|
||||
}, 200)
|
||||
|
||||
const observers = ref([])
|
||||
onMounted(() => {
|
||||
refreshIsOverflow()
|
||||
|
||||
observers.value.push(useResize(document.body, refreshIsOverflow))
|
||||
observers.value.push(useResize(content.value, refreshIsOverflow))
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
observers.value.forEach((item) => {
|
||||
item?.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
function handleScroll(x, width) {
|
||||
const wrapperWidth = wrapper.value?.offsetWidth
|
||||
const contentWidth = content.value?.offsetWidth
|
||||
if (contentWidth <= wrapperWidth) return
|
||||
|
||||
// 当 x 小于可视范围的最小值时
|
||||
if (x < -translateX.value + 150) {
|
||||
translateX.value = -(x - 150)
|
||||
resetTranslateX(wrapperWidth, contentWidth)
|
||||
}
|
||||
|
||||
// 当 x 大于可视范围的最大值时
|
||||
if (x + width > -translateX.value + wrapperWidth) {
|
||||
translateX.value = wrapperWidth - (x + width)
|
||||
resetTranslateX(wrapperWidth, contentWidth)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleScroll,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
|
||||
z-index: 9;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
.content {
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
transition: transform 0.5s;
|
||||
&.overflow {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
}
|
||||
.left,
|
||||
.right {
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
|
||||
width: 20px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 18px;
|
||||
border: 1px solid #e0e0e6;
|
||||
border-radius: 2px;
|
||||
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.left {
|
||||
left: 0;
|
||||
}
|
||||
.right {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
src/components/icon/CustomIcon.vue
Normal file
22
src/components/icon/CustomIcon.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
/** 自定义图标 */
|
||||
const props = defineProps({
|
||||
/** 图标名称(assets/svg下的文件名) */
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TheIcon type="custom" v-bind="props" />
|
||||
</template>
|
||||
24
src/components/icon/SvgIcon.vue
Normal file
24
src/components/icon/SvgIcon.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'icon-custom',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'currentColor',
|
||||
},
|
||||
})
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.icon}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg aria-hidden="true" width="1em" height="1em">
|
||||
<use :xlink:href="symbolId" :fill="color" />
|
||||
</svg>
|
||||
</template>
|
||||
33
src/components/icon/TheIcon.vue
Normal file
33
src/components/icon/TheIcon.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import { renderIcon, renderCustomIcon } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 14,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
/** iconify | custom */
|
||||
type: {
|
||||
type: String,
|
||||
default: 'iconify',
|
||||
},
|
||||
})
|
||||
|
||||
const iconCom = computed(() =>
|
||||
props.type === 'iconify'
|
||||
? renderIcon(props.icon, { size: props.size, color: props.color })
|
||||
: renderCustomIcon(props.icon, { size: props.size, color: props.color })
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="iconCom" />
|
||||
</template>
|
||||
18
src/components/page/AppPage.vue
Normal file
18
src/components/page/AppPage.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<section class="cus-scroll-y wh-full flex-col bg-[#f5f6fb] p-15 dark:bg-hex-121212">
|
||||
<slot />
|
||||
<!-- <AppFooter v-if="showFooter" mt-15 /> -->
|
||||
<n-back-top :bottom="20" />
|
||||
</section>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
33
src/components/page/CommonPage.vue
Normal file
33
src/components/page/CommonPage.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<AppPage :show-footer="showFooter">
|
||||
<header v-if="showHeader" mb-15 min-h-45 flex items-center justify-between px-15>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
<template v-else>
|
||||
<h2 text-22 font-normal text-hex-333 dark:text-hex-ccc>{{ title || route.meta?.title }}</h2>
|
||||
<slot name="action" />
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<n-card flex-1 rounded-10>
|
||||
<slot />
|
||||
</n-card>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
const route = useRoute()
|
||||
</script>
|
||||
27
src/components/query-bar/QueryBar.vue
Normal file
27
src/components/query-bar/QueryBar.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div
|
||||
bg="#fafafc"
|
||||
min-h-60
|
||||
flex
|
||||
items-start
|
||||
justify-between
|
||||
b-1
|
||||
rounded-8
|
||||
p-15
|
||||
bc-ccc
|
||||
dark:bg-black
|
||||
>
|
||||
<n-space wrap :size="[35, 15]">
|
||||
<slot />
|
||||
</n-space>
|
||||
|
||||
<div flex-shrink-0>
|
||||
<n-button secondary type="primary" @click="emit('reset')">重置</n-button>
|
||||
<n-button ml-20 type="primary" @click="emit('search')">搜索</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const emit = defineEmits(['search', 'reset'])
|
||||
</script>
|
||||
34
src/components/query-bar/QueryBarItem.vue
Normal file
34
src/components/query-bar/QueryBarItem.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div flex items-center>
|
||||
<label
|
||||
v-if="!isNullOrWhitespace(label)"
|
||||
w-80
|
||||
flex-shrink-0
|
||||
:style="{ width: labelWidth + 'px' }"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<div :style="{ width: contentWidth + 'px' }" flex-shrink-0>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isNullOrWhitespace } from '@/utils'
|
||||
|
||||
defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
labelWidth: {
|
||||
type: Number,
|
||||
default: 80,
|
||||
},
|
||||
contentWidth: {
|
||||
type: Number,
|
||||
default: 220,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
55
src/components/table/CrudModal.vue
Normal file
55
src/components/table/CrudModal.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
:style="{ width }"
|
||||
preset="card"
|
||||
:title="title"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
>
|
||||
<slot />
|
||||
<template v-if="showFooter" #footer>
|
||||
<footer flex justify-end>
|
||||
<slot name="footer">
|
||||
<n-button @click="show = false">取消</n-button>
|
||||
<n-button :loading="loading" ml-20 type="primary" @click="emit('onSave')">保存</n-button>
|
||||
</slot>
|
||||
</footer>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
default: '600px',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'onSave'])
|
||||
const show = computed({
|
||||
get() {
|
||||
return props.visible
|
||||
},
|
||||
set(v) {
|
||||
emit('update:visible', v)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
149
src/components/table/CrudTable.vue
Normal file
149
src/components/table/CrudTable.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<QueryBar v-if="$slots.queryBar" mb-30 @search="handleSearch" @reset="handleReset">
|
||||
<slot name="queryBar" />
|
||||
</QueryBar>
|
||||
|
||||
<n-data-table
|
||||
:remote="remote"
|
||||
:loading="loading"
|
||||
:scroll-x="scrollX"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:row-key="(row) => row[rowKey]"
|
||||
:pagination="isPagination ? pagination : false"
|
||||
@update:checked-row-keys="onChecked"
|
||||
@update:page="onPageChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { utils, writeFile } from 'xlsx'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* @remote true: 后端分页 false: 前端分页
|
||||
*/
|
||||
remote: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* @remote 是否分页
|
||||
*/
|
||||
isPagination: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
scrollX: {
|
||||
type: Number,
|
||||
default: 1200,
|
||||
},
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
/** queryBar中的参数 */
|
||||
queryItems: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
/** 补充参数(可选) */
|
||||
extraParams: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* ! 约定接口入参出参
|
||||
* * 分页模式需约定分页接口入参
|
||||
* @pageSize 分页参数:一页展示多少条,默认10
|
||||
* @pageNo 分页参数:页码,默认1
|
||||
* * 需约定接口出参
|
||||
* @pageData 分页模式必须,非分页模式如果没有pageData则取上一层data
|
||||
* @total 分页模式必须,非分页模式如果没有total则取上一层data.length
|
||||
*/
|
||||
getData: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:queryItems', 'onChecked', 'onDataChange'])
|
||||
const loading = ref(false)
|
||||
const initQuery = { ...props.queryItems }
|
||||
const tableData = ref([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10 })
|
||||
|
||||
async function handleQuery() {
|
||||
try {
|
||||
loading.value = true
|
||||
let paginationParams = {}
|
||||
// 如果非分页模式或者使用前端分页,则无需传分页参数
|
||||
if (props.isPagination && props.remote) {
|
||||
paginationParams = { pageNo: pagination.page, pageSize: pagination.pageSize }
|
||||
}
|
||||
const { data } = await props.getData({
|
||||
...props.queryItems,
|
||||
...props.extraParams,
|
||||
...paginationParams,
|
||||
})
|
||||
tableData.value = data?.pageData || data
|
||||
pagination.itemCount = data.total ?? data.length
|
||||
} catch (error) {
|
||||
tableData.value = []
|
||||
pagination.itemCount = 0
|
||||
} finally {
|
||||
emit('onDataChange', tableData.value)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
handleQuery()
|
||||
}
|
||||
async function handleReset() {
|
||||
const queryItems = { ...props.queryItems }
|
||||
for (const key in queryItems) {
|
||||
queryItems[key] = ''
|
||||
}
|
||||
emit('update:queryItems', { ...queryItems, ...initQuery })
|
||||
await nextTick()
|
||||
pagination.page = 1
|
||||
handleQuery()
|
||||
}
|
||||
function onPageChange(currentPage) {
|
||||
pagination.page = currentPage
|
||||
if (props.remote) {
|
||||
handleQuery()
|
||||
}
|
||||
}
|
||||
function onChecked(rowKeys) {
|
||||
if (props.columns.some((item) => item.type === 'selection')) {
|
||||
emit('onChecked', rowKeys)
|
||||
}
|
||||
}
|
||||
function handleExport(columns = props.columns, data = tableData.value) {
|
||||
if (!data?.length) return $message.warning('没有数据')
|
||||
const columnsData = columns.filter((item) => !!item.title && !item.hideInExcel)
|
||||
const thKeys = columnsData.map((item) => item.key)
|
||||
const thData = columnsData.map((item) => item.title)
|
||||
const trData = data.map((item) => thKeys.map((key) => item[key]))
|
||||
const sheet = utils.aoa_to_sheet([thData, ...trData])
|
||||
const workBook = utils.book_new()
|
||||
utils.book_append_sheet(workBook, sheet, '数据报表')
|
||||
writeFile(workBook, '数据报表.xlsx')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handleExport,
|
||||
})
|
||||
</script>
|
||||
1
src/composables/index.js
Normal file
1
src/composables/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useCRUD } from './useCRUD'
|
||||
103
src/composables/useCRUD.js
Normal file
103
src/composables/useCRUD.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { isNullOrWhitespace } from '@/utils'
|
||||
|
||||
const ACTIONS = {
|
||||
view: '查看',
|
||||
edit: '编辑',
|
||||
add: '新增',
|
||||
}
|
||||
|
||||
export default function ({ name, initForm = {}, doCreate, doDelete, doUpdate, refresh }) {
|
||||
const modalVisible = ref(false)
|
||||
const modalAction = ref('')
|
||||
const modalTitle = computed(() => ACTIONS[modalAction.value] + name)
|
||||
const modalLoading = ref(false)
|
||||
const modalFormRef = ref(null)
|
||||
const modalForm = ref({ ...initForm })
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
modalAction.value = 'add'
|
||||
modalVisible.value = true
|
||||
modalForm.value = { ...initForm }
|
||||
}
|
||||
|
||||
/** 修改 */
|
||||
function handleEdit(row) {
|
||||
modalAction.value = 'edit'
|
||||
modalVisible.value = true
|
||||
modalForm.value = { ...row }
|
||||
}
|
||||
|
||||
/** 查看 */
|
||||
function handleView(row) {
|
||||
modalAction.value = 'view'
|
||||
modalVisible.value = true
|
||||
modalForm.value = { ...row }
|
||||
}
|
||||
|
||||
/** 保存 */
|
||||
function handleSave() {
|
||||
if (!['edit', 'add'].includes(modalAction.value)) {
|
||||
modalVisible.value = false
|
||||
return
|
||||
}
|
||||
modalFormRef.value?.validate(async (err) => {
|
||||
if (err) return
|
||||
const actions = {
|
||||
add: {
|
||||
api: () => doCreate(modalForm.value),
|
||||
cb: () => $message.success('新增成功'),
|
||||
},
|
||||
edit: {
|
||||
api: () => doUpdate(modalForm.value),
|
||||
cb: () => $message.success('编辑成功'),
|
||||
},
|
||||
}
|
||||
const action = actions[modalAction.value]
|
||||
|
||||
try {
|
||||
modalLoading.value = true
|
||||
const data = await action.api()
|
||||
action.cb()
|
||||
modalLoading.value = modalVisible.value = false
|
||||
data && refresh(data)
|
||||
} catch (error) {
|
||||
modalLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete(id, confirmOptions) {
|
||||
if (isNullOrWhitespace(id)) return
|
||||
$dialog.confirm({
|
||||
content: '确定删除?',
|
||||
async confirm() {
|
||||
try {
|
||||
modalLoading.value = true
|
||||
const data = await doDelete(id)
|
||||
$message.success('删除成功')
|
||||
modalLoading.value = false
|
||||
refresh(data)
|
||||
} catch (error) {
|
||||
modalLoading.value = false
|
||||
}
|
||||
},
|
||||
...confirmOptions,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
modalVisible,
|
||||
modalAction,
|
||||
modalTitle,
|
||||
modalLoading,
|
||||
handleAdd,
|
||||
handleDelete,
|
||||
handleEdit,
|
||||
handleView,
|
||||
handleSave,
|
||||
modalForm,
|
||||
modalFormRef,
|
||||
}
|
||||
}
|
||||
43
src/hooks/useScript.js
Normal file
43
src/hooks/useScript.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
export function useScript(opts) {
|
||||
const isLoading = ref(false)
|
||||
const error = ref(false)
|
||||
const success = ref(false)
|
||||
let script
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
onMounted(() => {
|
||||
script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.charset = 'utf-8'
|
||||
script.onload = function () {
|
||||
isLoading.value = false
|
||||
success.value = true
|
||||
error.value = false
|
||||
resolve('')
|
||||
}
|
||||
|
||||
script.onerror = function (err) {
|
||||
isLoading.value = false
|
||||
success.value = false
|
||||
error.value = true
|
||||
reject(err)
|
||||
}
|
||||
|
||||
script.src = opts.src
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
script && script.remove()
|
||||
})
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
success,
|
||||
toPromise: () => promise,
|
||||
}
|
||||
}
|
||||
18
src/layout/components/AppMain.vue
Normal file
18
src/layout/components/AppMain.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<KeepAlive :include="keepAliveNames">
|
||||
<div h-full w-full>
|
||||
<component :is="Component" v-if="!tagStore.reloading" :key="route.fullPath" />
|
||||
</div>
|
||||
</KeepAlive>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTagsStore } from '@/store'
|
||||
const tagStore = useTagsStore()
|
||||
|
||||
const keepAliveNames = computed(() => {
|
||||
return tagStore.tags.filter((item) => item.keepAlive).map((item) => item.name)
|
||||
})
|
||||
</script>
|
||||
30
src/layout/components/header/components/BreadCrumb.vue
Normal file
30
src/layout/components/header/components/BreadCrumb.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<n-breadcrumb>
|
||||
<n-breadcrumb-item
|
||||
v-for="item in route.matched.filter((item) => !!item.meta?.title)"
|
||||
:key="item.path"
|
||||
@click="handleBreadClick(item.path)"
|
||||
>
|
||||
<component :is="getIcon(item.meta)" />
|
||||
{{ item.meta.title }}
|
||||
</n-breadcrumb-item>
|
||||
</n-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { renderCustomIcon, renderIcon } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
function handleBreadClick(path) {
|
||||
if (path === route.path) return
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function getIcon(meta) {
|
||||
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
|
||||
if (meta?.icon) return renderIcon(meta.icon, { size: 18 })
|
||||
return null
|
||||
}
|
||||
</script>
|
||||
12
src/layout/components/header/components/FullScreen.vue
Normal file
12
src/layout/components/header/components/FullScreen.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<n-icon mr20 size="18" style="cursor: pointer" @click="toggle">
|
||||
<icon-ant-design:fullscreen-exit-outlined v-if="isFullscreen" />
|
||||
<icon-ant-design:fullscreen-outlined v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
</script>
|
||||
11
src/layout/components/header/components/GithubSite.vue
Normal file
11
src/layout/components/header/components/GithubSite.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<n-icon mr-20 size="18" style="cursor: pointer" @click="handleLinkClick">
|
||||
<icon-mdi:github />
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
function handleLinkClick() {
|
||||
window.open('https://github.com/zclzone/vue-naive-admin')
|
||||
}
|
||||
</script>
|
||||
12
src/layout/components/header/components/MenuCollapse.vue
Normal file
12
src/layout/components/header/components/MenuCollapse.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<n-icon size="20" cursor-pointer @click="appStore.switchCollapsed">
|
||||
<icon-mdi:format-indent-increase v-if="appStore.collapsed" />
|
||||
<icon-mdi:format-indent-decrease v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<n-popover trigger="click" placement="bottom" @update:show="handlePopoverShow">
|
||||
<template #trigger>
|
||||
<n-badge :value="count" mr-20 cursor-pointer>
|
||||
<n-icon size="18" color-black dark="color-hex-fff">
|
||||
<icon-material-symbols:notifications-outline />
|
||||
</n-icon>
|
||||
</n-badge>
|
||||
</template>
|
||||
<n-tabs v-model:value="activeTab" type="line" justify-content="space-around" animated>
|
||||
<n-tab-pane
|
||||
v-for="tab in tabs"
|
||||
:key="tab.name"
|
||||
:name="tab.name"
|
||||
:tab="tab.title + `(${tab.messages.length})`"
|
||||
>
|
||||
<ul class="cus-scroll-y max-h-200 w-220">
|
||||
<li
|
||||
v-for="(item, index) in tab.messages"
|
||||
:key="index"
|
||||
class="flex-col py-12"
|
||||
border-t="1 solid gray-200"
|
||||
:style="index > 0 ? '' : 'border: none;'"
|
||||
>
|
||||
<span mb-4 text-ellipsis>{{ item.content }}</span>
|
||||
<span text-hex-bbb>{{ item.time }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const activeTab = ref('')
|
||||
const tabs = [
|
||||
{
|
||||
name: 'zan',
|
||||
title: '点赞',
|
||||
messages: [
|
||||
{ content: '你的文章《XX》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《YY》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《AA》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《BB》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《CC》收到一条点赞', time: formatDateTime() },
|
||||
{ content: '你的文章《DD》收到一条点赞', time: formatDateTime() },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'star',
|
||||
title: '关注',
|
||||
messages: [
|
||||
{ content: '张三 关注了你', time: formatDateTime() },
|
||||
{ content: '王五 关注了你', time: formatDateTime() },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'comment',
|
||||
title: '评论',
|
||||
messages: [
|
||||
{ content: '张三 评论了你的文章《XX》"学到了"', time: formatDateTime() },
|
||||
{ content: '李四 评论了你的文章《YY》"不如Vue"', time: formatDateTime() },
|
||||
],
|
||||
},
|
||||
]
|
||||
const count = ref(tabs.map((item) => item.messages).flat().length)
|
||||
|
||||
watch(activeTab, (v) => {
|
||||
if (count.value === 0) return
|
||||
const tabIndex = tabs.findIndex((item) => item.name === v)
|
||||
count.value -= tabs[tabIndex].messages.length
|
||||
if (count.value < 0) count.value = 0
|
||||
})
|
||||
|
||||
function handlePopoverShow(show) {
|
||||
if (show && !activeTab.value) {
|
||||
activeTab.value = tabs[0]?.name
|
||||
}
|
||||
}
|
||||
</script>
|
||||
18
src/layout/components/header/components/ThemeMode.vue
Normal file
18
src/layout/components/header/components/ThemeMode.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isDark = useDark()
|
||||
const toggleDark = () => {
|
||||
appStore.toggleDark()
|
||||
useToggle(isDark)()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-icon mr-20 cursor-pointer size="18" @click="toggleDark">
|
||||
<icon-mdi-moon-waning-crescent v-if="isDark" />
|
||||
<icon-mdi-white-balance-sunny v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
37
src/layout/components/header/components/UserAvatar.vue
Normal file
37
src/layout/components/header/components/UserAvatar.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<n-dropdown :options="options" @select="handleSelect">
|
||||
<div flex cursor-pointer items-center>
|
||||
<img :src="userStore.avatar" mr10 h-35 w-35 rounded-full />
|
||||
<span>{{ userStore.name }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
icon: renderIcon('mdi:exit-to-app', { size: '14px' }),
|
||||
},
|
||||
]
|
||||
|
||||
function handleSelect(key) {
|
||||
if (key === 'logout') {
|
||||
$dialog.confirm({
|
||||
title: '提示',
|
||||
type: 'info',
|
||||
content: '确认退出?',
|
||||
confirm() {
|
||||
userStore.logout()
|
||||
$message.success('已退出登录')
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
23
src/layout/components/header/index.vue
Normal file
23
src/layout/components/header/index.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div flex items-center>
|
||||
<MenuCollapse />
|
||||
<BreadCrumb ml-15 hidden sm:block />
|
||||
</div>
|
||||
<div ml-auto flex items-center>
|
||||
<!-- <MessageNotification /> -->
|
||||
<ThemeMode />
|
||||
<!-- <GithubSite /> -->
|
||||
<FullScreen />
|
||||
<UserAvatar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BreadCrumb from './components/BreadCrumb.vue'
|
||||
import MenuCollapse from './components/MenuCollapse.vue'
|
||||
import FullScreen from './components/FullScreen.vue'
|
||||
import UserAvatar from './components/UserAvatar.vue'
|
||||
// import GithubSite from './components/GithubSite.vue'
|
||||
import ThemeMode from './components/ThemeMode.vue'
|
||||
// import MessageNotification from './components/MessageNotification.vue'
|
||||
</script>
|
||||
15
src/layout/components/sidebar/components/SideLogo.vue
Normal file
15
src/layout/components/sidebar/components/SideLogo.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<router-link h-60 f-c-c to="/">
|
||||
<img src="@/assets/images/logo.png" height="42" />
|
||||
<h2 v-show="!appStore.collapsed" ml-10 max-w-140 flex-shrink-0 text-16 font-bold color-primary>
|
||||
{{ title }}
|
||||
</h2>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/store'
|
||||
const title = import.meta.env.VITE_TITLE
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
120
src/layout/components/sidebar/components/SideMenu.vue
Normal file
120
src/layout/components/sidebar/components/SideMenu.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<n-menu
|
||||
ref="menu"
|
||||
class="side-menu"
|
||||
accordion
|
||||
:indent="18"
|
||||
:collapsed-icon-size="22"
|
||||
:collapsed-width="64"
|
||||
:options="menuOptions"
|
||||
:value="activeKey"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePermissionStore } from '@/store'
|
||||
import { renderCustomIcon, renderIcon, isExternal } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
const curRoute = useRoute()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const activeKey = computed(() => curRoute.meta?.activeMenu || curRoute.name)
|
||||
|
||||
const menuOptions = computed(() => {
|
||||
return permissionStore.menus.map((item) => getMenuItem(item)).sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
const menu = ref(null)
|
||||
watch(curRoute, async () => {
|
||||
await nextTick()
|
||||
menu.value?.showOption()
|
||||
})
|
||||
|
||||
function resolvePath(basePath, path) {
|
||||
if (isExternal(path)) return path
|
||||
return (
|
||||
'/' +
|
||||
[basePath, path]
|
||||
.filter((path) => !!path && path !== '/')
|
||||
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
|
||||
.join('/')
|
||||
)
|
||||
}
|
||||
|
||||
function getMenuItem(route, basePath = '') {
|
||||
let menuItem = {
|
||||
label: (route.meta && route.meta.title) || route.name,
|
||||
key: route.name,
|
||||
path: resolvePath(basePath, route.path),
|
||||
icon: getIcon(route.meta),
|
||||
order: route.meta?.order || 0,
|
||||
}
|
||||
|
||||
const visibleChildren = route.children
|
||||
? route.children.filter((item) => item.name && !item.isHidden)
|
||||
: []
|
||||
|
||||
if (!visibleChildren.length) return menuItem
|
||||
|
||||
// if (visibleChildren.length === 1) {
|
||||
// // 单个子路由处理
|
||||
// const singleRoute = visibleChildren[0]
|
||||
// menuItem = {
|
||||
// ...menuItem,
|
||||
// label: singleRoute.meta?.title || singleRoute.name,
|
||||
// key: singleRoute.name,
|
||||
// path: resolvePath(menuItem.path, singleRoute.path),
|
||||
// icon: getIcon(singleRoute.meta),
|
||||
// }
|
||||
// const visibleItems = singleRoute.children
|
||||
// ? singleRoute.children.filter((item) => item.name && !item.isHidden)
|
||||
// : []
|
||||
|
||||
// if (visibleItems.length === 1) {
|
||||
// menuItem = getMenuItem(visibleItems[0], menuItem.path)
|
||||
// } else if (visibleItems.length > 1) {
|
||||
// menuItem.children = visibleItems
|
||||
// .map((item) => getMenuItem(item, menuItem.path))
|
||||
// .sort((a, b) => a.order - b.order)
|
||||
// }
|
||||
// } else {
|
||||
menuItem.children = visibleChildren
|
||||
.map((item) => getMenuItem(item, menuItem.path))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
// }
|
||||
return menuItem
|
||||
}
|
||||
|
||||
function getIcon(meta) {
|
||||
if (meta?.customIcon) return renderCustomIcon(meta.customIcon, { size: 18 })
|
||||
if (meta?.icon !== '无' && meta?.icon) return renderIcon(meta.icon, { size: 18 })
|
||||
return null
|
||||
}
|
||||
|
||||
function handleMenuSelect(_, item) {
|
||||
if (isExternal(item.path)) {
|
||||
window.open(item.path)
|
||||
} else {
|
||||
router.push(item.path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.side-menu:not(.n-menu--collapsed) {
|
||||
.n-menu-item-content {
|
||||
&::before {
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
&.n-menu-item-content--selected,
|
||||
&:hover {
|
||||
&::before {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
9
src/layout/components/sidebar/index.vue
Normal file
9
src/layout/components/sidebar/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import SideLogo from './components/SideLogo.vue'
|
||||
import SideMenu from './components/SideMenu.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SideLogo />
|
||||
<SideMenu />
|
||||
</template>
|
||||
118
src/layout/components/tags/ContextMenu.vue
Normal file
118
src/layout/components/tags/ContextMenu.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<n-dropdown
|
||||
:show="show"
|
||||
:options="options"
|
||||
:x="x"
|
||||
:y="y"
|
||||
placement="bottom-start"
|
||||
@clickoutside="handleHideDropdown"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTagsStore } from '@/store'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
currentPath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
const tagsStore = useTagsStore()
|
||||
|
||||
const options = computed(() => [
|
||||
{
|
||||
label: '重新加载',
|
||||
key: 'reload',
|
||||
disabled: props.currentPath !== tagsStore.activeTag,
|
||||
icon: renderIcon('mdi:refresh', { size: '14px' }),
|
||||
},
|
||||
{
|
||||
label: '关闭',
|
||||
key: 'close',
|
||||
disabled: tagsStore.tags.length <= 1,
|
||||
icon: renderIcon('mdi:close', { size: '14px' }),
|
||||
},
|
||||
{
|
||||
label: '关闭其他',
|
||||
key: 'close-other',
|
||||
disabled: tagsStore.tags.length <= 1,
|
||||
icon: renderIcon('mdi:arrow-expand-horizontal', { size: '14px' }),
|
||||
},
|
||||
{
|
||||
label: '关闭左侧',
|
||||
key: 'close-left',
|
||||
disabled: tagsStore.tags.length <= 1 || props.currentPath === tagsStore.tags[0].path,
|
||||
icon: renderIcon('mdi:arrow-expand-left', { size: '14px' }),
|
||||
},
|
||||
{
|
||||
label: '关闭右侧',
|
||||
key: 'close-right',
|
||||
disabled:
|
||||
tagsStore.tags.length <= 1 ||
|
||||
props.currentPath === tagsStore.tags[tagsStore.tags.length - 1].path,
|
||||
icon: renderIcon('mdi:arrow-expand-right', { size: '14px' }),
|
||||
},
|
||||
])
|
||||
|
||||
const route = useRoute()
|
||||
const actionMap = new Map([
|
||||
[
|
||||
'reload',
|
||||
() => {
|
||||
tagsStore.reloadTag(route.path, route.meta?.keepAlive)
|
||||
},
|
||||
],
|
||||
[
|
||||
'close',
|
||||
() => {
|
||||
tagsStore.removeTag(props.currentPath)
|
||||
},
|
||||
],
|
||||
[
|
||||
'close-other',
|
||||
() => {
|
||||
tagsStore.removeOther(props.currentPath)
|
||||
},
|
||||
],
|
||||
[
|
||||
'close-left',
|
||||
() => {
|
||||
tagsStore.removeLeft(props.currentPath)
|
||||
},
|
||||
],
|
||||
[
|
||||
'close-right',
|
||||
() => {
|
||||
tagsStore.removeRight(props.currentPath)
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
function handleHideDropdown() {
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
function handleSelect(key) {
|
||||
const actionFn = actionMap.get(key)
|
||||
actionFn && actionFn()
|
||||
handleHideDropdown()
|
||||
}
|
||||
</script>
|
||||
106
src/layout/components/tags/index.vue
Normal file
106
src/layout/components/tags/index.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<ScrollX ref="scrollXRef" class="bg-white dark:bg-dark!">
|
||||
<n-tag
|
||||
v-for="tag in tagsStore.tags"
|
||||
ref="tabRefs"
|
||||
:key="tag.path"
|
||||
class="mx-5 cursor-pointer rounded-4 px-15 hover:color-primary"
|
||||
:type="tagsStore.activeTag === tag.path ? 'primary' : 'default'"
|
||||
:closable="tagsStore.tags.length > 1"
|
||||
@click="handleTagClick(tag.path)"
|
||||
@close.stop="tagsStore.removeTag(tag.path)"
|
||||
@contextmenu.prevent="handleContextMenu($event, tag)"
|
||||
>
|
||||
<template v-if="tag.icon" #icon>
|
||||
<TheIcon :icon="tag.icon" class="mr-4" />
|
||||
</template>
|
||||
{{ tag.title }}
|
||||
</n-tag>
|
||||
<ContextMenu
|
||||
v-if="contextMenuOption.show"
|
||||
v-model:show="contextMenuOption.show"
|
||||
:current-path="contextMenuOption.currentPath"
|
||||
:x="contextMenuOption.x"
|
||||
:y="contextMenuOption.y"
|
||||
/>
|
||||
</ScrollX>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import { useTagsStore } from '@/store'
|
||||
import ScrollX from '@/components/common/ScrollX.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tagsStore = useTagsStore()
|
||||
const tabRefs = ref([])
|
||||
const scrollXRef = ref(null)
|
||||
|
||||
const contextMenuOption = reactive({
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
currentPath: '',
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
const { name, fullPath: path } = route
|
||||
const title = route.meta?.title
|
||||
const icon = route.meta?.icon
|
||||
const keepAlive = route.meta?.keepAlive
|
||||
tagsStore.addTag({ name, path, title, icon, keepAlive })
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => tagsStore.activeIndex,
|
||||
async (activeIndex) => {
|
||||
await nextTick()
|
||||
const activeTabElement = tabRefs.value[activeIndex]?.$el
|
||||
if (!activeTabElement) return
|
||||
const { offsetLeft: x, offsetWidth: width } = activeTabElement
|
||||
scrollXRef.value?.handleScroll(x + width, width)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleTagClick = (path) => {
|
||||
tagsStore.setActiveTag(path)
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function showContextMenu() {
|
||||
contextMenuOption.show = true
|
||||
}
|
||||
function hideContextMenu() {
|
||||
contextMenuOption.show = false
|
||||
}
|
||||
function setContextMenu(x, y, currentPath) {
|
||||
Object.assign(contextMenuOption, { x, y, currentPath })
|
||||
}
|
||||
|
||||
// 右击菜单
|
||||
async function handleContextMenu(e, tagItem) {
|
||||
const { clientX, clientY } = e
|
||||
hideContextMenu()
|
||||
setContextMenu(clientX, clientY, tagItem.path)
|
||||
await nextTick()
|
||||
showContextMenu()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.n-tag__close {
|
||||
box-sizing: content-box;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
transform: scale(0.9);
|
||||
transform: translateX(5px);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
</style>
|
||||
42
src/layout/index.vue
Normal file
42
src/layout/index.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<n-layout has-sider wh-full>
|
||||
<n-layout-sider
|
||||
bordered
|
||||
collapse-mode="width"
|
||||
:collapsed-width="64"
|
||||
:width="220"
|
||||
:native-scrollbar="false"
|
||||
:collapsed="appStore.collapsed"
|
||||
>
|
||||
<SideBar />
|
||||
</n-layout-sider>
|
||||
|
||||
<article flex-col flex-1 overflow-hidden>
|
||||
<header
|
||||
border-b="1 solid #eee"
|
||||
class="flex items-center bg-white px-15"
|
||||
dark="bg-dark border-0"
|
||||
:style="`height: ${header.height}px`"
|
||||
>
|
||||
<AppHeader />
|
||||
</header>
|
||||
<section v-if="tags.visible" hidden border-b bc-eee sm:block dark:border-0>
|
||||
<AppTags :style="{ height: `${tags.height}px` }" />
|
||||
</section>
|
||||
<section flex-1 overflow-hidden bg-hex-f5f6fb dark:bg-hex-101014>
|
||||
<AppMain />
|
||||
</section>
|
||||
</article>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AppHeader from './components/header/index.vue'
|
||||
import SideBar from './components/sidebar/index.vue'
|
||||
import AppMain from './components/AppMain.vue'
|
||||
import AppTags from './components/tags/index.vue'
|
||||
import { useAppStore } from '@/store'
|
||||
import { header, tags } from '~/settings'
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
49
src/main.js
Normal file
49
src/main.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/** 重置样式 */
|
||||
import '@/styles/reset.css'
|
||||
import 'uno.css'
|
||||
import '@/styles/global.scss'
|
||||
import 'virtual:svg-icons-register'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { setupRouter } from '@/router'
|
||||
import { setupStore } from '@/store'
|
||||
import App from './App.vue'
|
||||
import { setupNaiveDiscreteApi } from './utils'
|
||||
import { initApiEndpoint } from '@/utils/api-config'
|
||||
|
||||
async function setupApp() {
|
||||
const app = createApp(App)
|
||||
|
||||
// 初始化接口配置
|
||||
initApiEndpoint()
|
||||
|
||||
setupStore(app)
|
||||
setupNaiveDiscreteApi()
|
||||
|
||||
await setupRouter(app)
|
||||
|
||||
app.directive('perms', {
|
||||
mounted: (el, binding) => {
|
||||
const { value } = binding
|
||||
const permissions = JSON.parse(localStorage.getItem('roles'))
|
||||
const all_permission = '*'
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
const hasPermission = permissions.some((key) => {
|
||||
return all_permission === key || value.includes(key)
|
||||
})
|
||||
|
||||
if (!hasPermission) {
|
||||
el.parentNode && el.parentNode.removeChild(el)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('like v-perms="[\'auth/menu/edit\']"')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
setupApp()
|
||||
9
src/router/guard/index.js
Normal file
9
src/router/guard/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createPageLoadingGuard } from './page-loading-guard'
|
||||
import { createPageTitleGuard } from './page-title-guard'
|
||||
import { createPermissionGuard } from './permission-guard'
|
||||
|
||||
export function setupRouterGuard(router) {
|
||||
createPageLoadingGuard(router)
|
||||
createPermissionGuard(router)
|
||||
createPageTitleGuard(router)
|
||||
}
|
||||
15
src/router/guard/page-loading-guard.js
Normal file
15
src/router/guard/page-loading-guard.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export function createPageLoadingGuard(router) {
|
||||
router.beforeEach(() => {
|
||||
window.$loadingBar?.start()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
setTimeout(() => {
|
||||
window.$loadingBar?.finish()
|
||||
}, 200)
|
||||
})
|
||||
|
||||
router.onError(() => {
|
||||
window.$loadingBar?.error()
|
||||
})
|
||||
}
|
||||
12
src/router/guard/page-title-guard.js
Normal file
12
src/router/guard/page-title-guard.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const baseTitle = import.meta.env.VITE_TITLE
|
||||
|
||||
export function createPageTitleGuard(router) {
|
||||
router.afterEach((to) => {
|
||||
const pageTitle = to.meta?.title
|
||||
if (pageTitle) {
|
||||
document.title = `${pageTitle} | ${baseTitle}`
|
||||
} else {
|
||||
document.title = baseTitle
|
||||
}
|
||||
})
|
||||
}
|
||||
34
src/router/guard/permission-guard.js
Normal file
34
src/router/guard/permission-guard.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { getToken, isNullOrWhitespace } from '@/utils'
|
||||
import { addDynamicRoutes } from '@/router'
|
||||
|
||||
const WHITE_LIST = ['/login', '/404']
|
||||
export function createPermissionGuard(router) {
|
||||
router.beforeEach(async (to) => {
|
||||
const token = getToken()
|
||||
|
||||
/** 没有token的情况 */
|
||||
if (isNullOrWhitespace(token)) {
|
||||
if (WHITE_LIST.includes(to.path)) return true
|
||||
return { path: 'login', query: { ...to.query, redirect: to.path } }
|
||||
}
|
||||
|
||||
/** 有token的情况 */
|
||||
if (to.path === '/login') return { path: '/' }
|
||||
|
||||
// 确保动态路由已加载
|
||||
if (token && !router.hasRoute('Dashboard')) {
|
||||
try {
|
||||
await addDynamicRoutes()
|
||||
// 如果当前路径不存在,重定向到工作台
|
||||
if (to.path !== '/' && !router.hasRoute(to.name)) {
|
||||
return { path: '/workbench' }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('动态路由加载失败:', error)
|
||||
return { path: '/login' }
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
87
src/router/index.js
Normal file
87
src/router/index.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import { setupRouterGuard } from './guard'
|
||||
import { basicRoutes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes'
|
||||
import { getToken, isNullOrWhitespace } from '@/utils'
|
||||
import { usePermissionStore } from '@/store'
|
||||
|
||||
const isHash = false
|
||||
export const router = createRouter({
|
||||
history: isHash ? createWebHashHistory('/') : createWebHistory('/'),
|
||||
routes: basicRoutes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
export async function setupRouter(app) {
|
||||
await addDynamicRoutes()
|
||||
setupRouterGuard(router)
|
||||
app.use(router)
|
||||
}
|
||||
|
||||
export async function addDynamicRoutes() {
|
||||
const token = getToken()
|
||||
|
||||
// 没有token情况
|
||||
if (isNullOrWhitespace(token)) {
|
||||
router.addRoute(EMPTY_ROUTE)
|
||||
return
|
||||
}
|
||||
|
||||
// 有token的情况
|
||||
try {
|
||||
const permissionStore = usePermissionStore()
|
||||
const accessRoutes = permissionStore.generateRoutes()
|
||||
|
||||
// 确保路由按正确顺序添加
|
||||
accessRoutes.forEach((route) => {
|
||||
if (!router.hasRoute(route.name)) {
|
||||
router.addRoute(route)
|
||||
}
|
||||
})
|
||||
|
||||
// 移除空路由,添加404路由
|
||||
if (router.hasRoute(EMPTY_ROUTE.name)) {
|
||||
router.removeRoute(EMPTY_ROUTE.name)
|
||||
}
|
||||
router.addRoute(NOT_FOUND_ROUTE)
|
||||
|
||||
// 确保根路径重定向到工作台
|
||||
// if (!router.hasRoute('workbench')) {
|
||||
// const workbenchRoute = {
|
||||
// name: 'workbench',
|
||||
// path: '/',
|
||||
// component: () => import('@/views/workbench/index.vue'),
|
||||
// redirect: '/workbench',
|
||||
// // children: [
|
||||
// // {
|
||||
// // name: 'Workbench',
|
||||
// // path: 'workbench',
|
||||
// // component: () => import('@/views/workbench/index.vue'),
|
||||
// // meta: {
|
||||
// // title: '工作台',
|
||||
// // icon: 'mdi:index',
|
||||
// // order: 0,
|
||||
// // },
|
||||
// // },
|
||||
// // ],
|
||||
// }
|
||||
// router.addRoute(workbenchRoute)
|
||||
// }
|
||||
|
||||
// console.log(router)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function getRouteNames(routes) {
|
||||
return routes.map((route) => getRouteName(route)).flat(1)
|
||||
}
|
||||
|
||||
function getRouteName(route) {
|
||||
const names = [route.name]
|
||||
if (route.subMenu && route.subMenu.length) {
|
||||
names.push(...route.subMenu.map((item) => getRouteName(item)).flat(1))
|
||||
}
|
||||
return names
|
||||
}
|
||||
31
src/router/routes/index.js
Normal file
31
src/router/routes/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export const basicRoutes = [
|
||||
{
|
||||
name: '404',
|
||||
path: '/404',
|
||||
component: () => import('@/views/error-page/404.vue'),
|
||||
isHidden: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Login',
|
||||
path: '/login',
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
isHidden: true,
|
||||
meta: {
|
||||
title: '登录页',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const NOT_FOUND_ROUTE = {
|
||||
name: 'NotFound',
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/404',
|
||||
isHidden: true,
|
||||
}
|
||||
|
||||
export const EMPTY_ROUTE = {
|
||||
name: 'Empty',
|
||||
path: '/:pathMatch(.*)*',
|
||||
component: null,
|
||||
}
|
||||
7
src/store/index.js
Normal file
7
src/store/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export function setupStore(app) {
|
||||
app.use(createPinia())
|
||||
}
|
||||
|
||||
export * from './modules'
|
||||
28
src/store/modules/app/index.js
Normal file
28
src/store/modules/app/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
const isDark = useDark()
|
||||
export const useAppStore = defineStore('app', {
|
||||
state() {
|
||||
return {
|
||||
collapsed: false,
|
||||
isDark,
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
switchCollapsed() {
|
||||
this.collapsed = !this.collapsed
|
||||
},
|
||||
setCollapsed(collapsed) {
|
||||
this.collapsed = collapsed
|
||||
},
|
||||
/** 设置暗黑模式 */
|
||||
setDark(isDark) {
|
||||
this.isDark = isDark
|
||||
},
|
||||
/** 切换/关闭 暗黑模式 */
|
||||
toggleDark() {
|
||||
this.isDark = !this.isDark
|
||||
},
|
||||
},
|
||||
})
|
||||
4
src/store/modules/index.js
Normal file
4
src/store/modules/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './app'
|
||||
export * from './permission'
|
||||
export * from './tags'
|
||||
export * from './user'
|
||||
132
src/store/modules/permission/index.js
Normal file
132
src/store/modules/permission/index.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { basicRoutes } from '@/router/routes'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
// 匹配views里面所有的.vue文件,动态引入
|
||||
const modules = import.meta.glob('/src/views/**/*.vue')
|
||||
|
||||
//
|
||||
export function getModulesKey() {
|
||||
return Object.keys(modules).map((item) => item.replace('/src/views/', '').replace('.vue', ''))
|
||||
}
|
||||
|
||||
// 动态加载组件
|
||||
export function loadRouteView(component) {
|
||||
try {
|
||||
const key = Object.keys(modules).find((key) => {
|
||||
return key.includes(`${component}.vue`)
|
||||
})
|
||||
if (key) {
|
||||
return modules[key]
|
||||
}
|
||||
throw Error(`找不到组件${component},请确保组件路径正确`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return RouterView
|
||||
}
|
||||
}
|
||||
|
||||
// function hasPermission(route, role) {
|
||||
// // * 不需要权限直接返回true
|
||||
// if (!route.meta?.requireAuth) return true
|
||||
|
||||
// const routeRole = route.meta?.role ? route.meta.role : []
|
||||
|
||||
// // * 登录用户没有角色或者路由没有设置角色判定为没有权限
|
||||
// if (!role.length || !routeRole.length) return false
|
||||
|
||||
// // * 路由指定的角色包含任一登录用户角色则判定有权限
|
||||
// return role.some((item) => routeRole.includes(item))
|
||||
// }
|
||||
|
||||
// 过滤异步路由
|
||||
function filterAsyncRoutes(routes = [], firstRoute = true) {
|
||||
const ret = []
|
||||
routes.forEach((route) => {
|
||||
// 过滤掉type为3的路由
|
||||
if (route.type === 3) return
|
||||
const isHidden = route.is_show === 1 ? false : true
|
||||
|
||||
const meta = {
|
||||
requireAuth: true,
|
||||
title: route.name,
|
||||
icon: route.icon,
|
||||
order: route.sort,
|
||||
}
|
||||
|
||||
const curRoute = {
|
||||
path: route.route,
|
||||
name: route.route,
|
||||
isHidden,
|
||||
meta,
|
||||
children: [],
|
||||
}
|
||||
|
||||
// if (route.route === '/' && firstRoute) {
|
||||
// curRoute['redirect'] = route.subMenu[0].route
|
||||
// } else if (route.subMenu && route.type === 1) {
|
||||
// curRoute['redirect'] = `${route.subMenu[0].route}`
|
||||
// }
|
||||
|
||||
if (route.subMenu && route.subMenu.length) {
|
||||
curRoute.children = filterAsyncRoutes(route.subMenu, false)
|
||||
} else {
|
||||
Reflect.deleteProperty(curRoute, 'children')
|
||||
}
|
||||
|
||||
switch (route.type) {
|
||||
case 1:
|
||||
curRoute.component = firstRoute ? Layout : RouterView
|
||||
break
|
||||
case 2:
|
||||
curRoute.component = loadRouteView(route.components)
|
||||
break
|
||||
}
|
||||
ret.push(curRoute)
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
// 递归寻找type为3的路由
|
||||
function findType3Routes(routes = []) {
|
||||
const ret = []
|
||||
routes.forEach((route) => {
|
||||
if (route.type === 3) {
|
||||
ret.push(route.api_route)
|
||||
}
|
||||
if (route.subMenu && route.subMenu.length) {
|
||||
ret.push(...findType3Routes(route.subMenu))
|
||||
}
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
state() {
|
||||
return {
|
||||
accessRoutes: [],
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
routes() {
|
||||
return basicRoutes.concat(this.accessRoutes)
|
||||
},
|
||||
menus() {
|
||||
return this.routes.filter((route) => route.name && !route.isHidden)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
generateRoutes() {
|
||||
const menus = JSON.parse(localStorage.getItem('menu'))
|
||||
const accessRoutes = filterAsyncRoutes(menus)
|
||||
window.localStorage.setItem('roles', JSON.stringify(findType3Routes(menus)))
|
||||
this.accessRoutes = accessRoutes
|
||||
return accessRoutes
|
||||
},
|
||||
resetPermission() {
|
||||
this.$reset()
|
||||
},
|
||||
},
|
||||
})
|
||||
6
src/store/modules/tags/helpers.js
Normal file
6
src/store/modules/tags/helpers.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { sStorage } from '@/utils'
|
||||
|
||||
export const activeTag = sStorage.get('activeTag')
|
||||
export const tags = sStorage.get('tags')
|
||||
|
||||
export const WITHOUT_TAG_PATHS = ['/404', '/login']
|
||||
83
src/store/modules/tags/index.js
Normal file
83
src/store/modules/tags/index.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { activeTag, tags, WITHOUT_TAG_PATHS } from './helpers'
|
||||
import { router } from '@/router'
|
||||
import { sStorage } from '@/utils'
|
||||
|
||||
export const useTagsStore = defineStore('tag', {
|
||||
state() {
|
||||
return {
|
||||
tags: tags || [],
|
||||
activeTag: activeTag || '',
|
||||
reloading: false,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
activeIndex() {
|
||||
return this.tags.findIndex((item) => item.path === this.activeTag)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setActiveTag(path) {
|
||||
this.activeTag = path
|
||||
sStorage.set('activeTag', path)
|
||||
},
|
||||
setTags(tags) {
|
||||
this.tags = tags
|
||||
sStorage.set('tags', tags)
|
||||
},
|
||||
addTag(tag = {}) {
|
||||
if (WITHOUT_TAG_PATHS.includes(tag.path)) return
|
||||
let findItem = this.tags.find((item) => item.path === tag.path)
|
||||
if (findItem) findItem = tag
|
||||
else this.setTags([...this.tags, tag])
|
||||
this.setActiveTag(tag.path)
|
||||
},
|
||||
async reloadTag(path, keepAlive) {
|
||||
const findItem = this.tags.find((item) => item.path === path)
|
||||
// 更新key可让keepAlive失效
|
||||
if (findItem && keepAlive) findItem.keepAlive = false
|
||||
|
||||
$loadingBar.start()
|
||||
this.reloading = true
|
||||
await nextTick()
|
||||
this.reloading = false
|
||||
findItem.keepAlive = keepAlive
|
||||
setTimeout(() => {
|
||||
document.documentElement.scrollTo({ left: 0, top: 0 })
|
||||
$loadingBar.finish()
|
||||
}, 100)
|
||||
},
|
||||
removeTag(path) {
|
||||
this.setTags(this.tags.filter((tag) => tag.path !== path))
|
||||
if (path === this.activeTag) {
|
||||
router.push(this.tags[this.tags.length - 1].path)
|
||||
}
|
||||
},
|
||||
removeOther(curPath = this.activeTag) {
|
||||
this.setTags(this.tags.filter((tag) => tag.path === curPath))
|
||||
if (curPath !== this.activeTag) {
|
||||
router.push(this.tags[this.tags.length - 1].path)
|
||||
}
|
||||
},
|
||||
removeLeft(curPath) {
|
||||
const curIndex = this.tags.findIndex((item) => item.path === curPath)
|
||||
const filterTags = this.tags.filter((item, index) => index >= curIndex)
|
||||
this.setTags(filterTags)
|
||||
if (!filterTags.find((item) => item.path === this.activeTag)) {
|
||||
router.push(filterTags[filterTags.length - 1].path)
|
||||
}
|
||||
},
|
||||
removeRight(curPath) {
|
||||
const curIndex = this.tags.findIndex((item) => item.path === curPath)
|
||||
const filterTags = this.tags.filter((item, index) => index <= curIndex)
|
||||
this.setTags(filterTags)
|
||||
if (!filterTags.find((item) => item.path === this.activeTag)) {
|
||||
router.push(filterTags[filterTags.length - 1].path)
|
||||
}
|
||||
},
|
||||
resetTags() {
|
||||
this.setTags([])
|
||||
this.setActiveTag('')
|
||||
},
|
||||
},
|
||||
})
|
||||
52
src/store/modules/user/index.js
Normal file
52
src/store/modules/user/index.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { defineStore } from 'pinia'
|
||||
// import { resetRouter } from '@/router'
|
||||
import { useTagsStore, usePermissionStore } from '@/store'
|
||||
import { removeToken, toLogin } from '@/utils'
|
||||
// import api from '@/api'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state() {
|
||||
return {
|
||||
userInfo: {},
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
userId() {
|
||||
return this.userInfo?.id
|
||||
},
|
||||
name() {
|
||||
return this.userInfo?.name
|
||||
},
|
||||
avatar() {
|
||||
return this.userInfo?.avatar || 'https://v2.xxapi.cn/api/head?return=302'
|
||||
},
|
||||
role() {
|
||||
return this.userInfo?.role || []
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async getUserInfo() {
|
||||
// try {
|
||||
// const res = await api.getUser()
|
||||
// const { id, name, avatar, role } = res.data
|
||||
// this.userInfo = { id, name, avatar, role }
|
||||
// return Promise.resolve(res.data)
|
||||
// } catch (error) {
|
||||
// return Promise.reject(error)
|
||||
// }
|
||||
},
|
||||
async logout() {
|
||||
const { resetTags } = useTagsStore()
|
||||
const { resetPermission } = usePermissionStore()
|
||||
removeToken()
|
||||
resetTags()
|
||||
resetPermission()
|
||||
// resetRouter()
|
||||
this.$reset()
|
||||
toLogin()
|
||||
},
|
||||
setUserInfo(userInfo = {}) {
|
||||
this.userInfo = { ...this.userInfo, ...userInfo }
|
||||
},
|
||||
},
|
||||
})
|
||||
74
src/styles/global.scss
Normal file
74
src/styles/global.scss
Normal file
@@ -0,0 +1,74 @@
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 4px; // * 1rem = 4px 方便unocss计算:在unocss中 1字体单位 = 0.25rem,相当于 1等份 = 1px
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* transition fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.cus-scroll {
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
.cus-scroll-x {
|
||||
overflow-x: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
.cus-scroll-y {
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
.cus-scroll,
|
||||
.cus-scroll-x,
|
||||
.cus-scroll-y {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #bfbfbf;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/styles/reset.css
Normal file
35
src/styles/reset.css
Normal file
@@ -0,0 +1,35 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:link,
|
||||
a:visited,
|
||||
a:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
outline: none;
|
||||
border: none;
|
||||
resize: none;
|
||||
}
|
||||
61
src/utils/api-config.js
Normal file
61
src/utils/api-config.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// 判断是否为开发环境
|
||||
const isDev = import.meta.env.DEV
|
||||
|
||||
// API接口线路配置管理
|
||||
export const API_ENDPOINTS = {
|
||||
primary: {
|
||||
name: '主要线路',
|
||||
baseURL: import.meta.env.VITE_BASE_API,
|
||||
timeout: 10000,
|
||||
},
|
||||
backup1: {
|
||||
name: '备用线路',
|
||||
baseURL: import.meta.env.VITE_BASE_API_1,
|
||||
timeout: 10000,
|
||||
},
|
||||
}
|
||||
|
||||
// 调试信息
|
||||
console.log('=== 多接口配置信息 ===')
|
||||
console.log('当前环境:', isDev ? '开发环境' : '生产环境')
|
||||
console.log('API配置:', API_ENDPOINTS)
|
||||
console.log('环境变量 VITE_BASE_API:', import.meta.env.VITE_BASE_API)
|
||||
console.log('环境变量 VITE_BASE_API_1:', import.meta.env.VITE_BASE_API_1)
|
||||
console.log('是否使用代理:', import.meta.env.VITE_USE_PROXY)
|
||||
|
||||
// 当前使用的接口线路
|
||||
let currentEndpoint = 'primary'
|
||||
|
||||
// 获取当前接口配置
|
||||
export function getCurrentEndpoint() {
|
||||
return {
|
||||
key: currentEndpoint,
|
||||
...API_ENDPOINTS[currentEndpoint],
|
||||
}
|
||||
}
|
||||
|
||||
// 设置当前接口
|
||||
export function setCurrentEndpoint(endpoint) {
|
||||
if (API_ENDPOINTS[endpoint]) {
|
||||
currentEndpoint = endpoint
|
||||
localStorage.setItem('api_endpoint', endpoint)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取所有可用接口
|
||||
export function getAvailableEndpoints() {
|
||||
return Object.entries(API_ENDPOINTS).map(([key, config]) => ({
|
||||
key,
|
||||
...config,
|
||||
}))
|
||||
}
|
||||
|
||||
// 初始化时从本地存储恢复接口选择
|
||||
export function initApiEndpoint() {
|
||||
const savedEndpoint = localStorage.getItem('api_endpoint')
|
||||
if (savedEndpoint && API_ENDPOINTS[savedEndpoint]) {
|
||||
currentEndpoint = savedEndpoint
|
||||
}
|
||||
}
|
||||
17
src/utils/auth/auth.js
Normal file
17
src/utils/auth/auth.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { router } from '@/router'
|
||||
|
||||
export function toLogin() {
|
||||
const currentRoute = unref(router.currentRoute)
|
||||
const needRedirect =
|
||||
!currentRoute.meta.requireAuth && !['/404', '/login'].includes(router.currentRoute.value.path)
|
||||
router.replace({
|
||||
path: '/login',
|
||||
query: needRedirect ? { ...currentRoute.query, redirect: currentRoute.path } : {},
|
||||
})
|
||||
}
|
||||
|
||||
export function toFourZeroFour() {
|
||||
router.replace({
|
||||
path: '/404',
|
||||
})
|
||||
}
|
||||
2
src/utils/auth/index.js
Normal file
2
src/utils/auth/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './auth'
|
||||
export * from './token'
|
||||
33
src/utils/auth/token.js
Normal file
33
src/utils/auth/token.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { lStorage } from '@/utils'
|
||||
import api from '@/api'
|
||||
|
||||
const TOKEN_CODE = 'access_token'
|
||||
const DURATION = 6 * 60 * 60
|
||||
|
||||
export function getToken() {
|
||||
return lStorage.get(TOKEN_CODE)
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
lStorage.set(TOKEN_CODE, token, DURATION)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
lStorage.remove(TOKEN_CODE)
|
||||
}
|
||||
|
||||
export async function refreshAccessToken() {
|
||||
const tokenItem = lStorage.getItem(TOKEN_CODE)
|
||||
if (!tokenItem) {
|
||||
return
|
||||
}
|
||||
const { time } = tokenItem
|
||||
// token生成或者刷新后30分钟内不执行刷新
|
||||
if (new Date().getTime() - time <= 1000 * 60 * 30) return
|
||||
try {
|
||||
const res = await api.refreshToken()
|
||||
setToken(res.data.token)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
90
src/utils/common/common.js
Normal file
90
src/utils/common/common.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* @desc 格式化时间
|
||||
* @param {(Object|string|number)} time
|
||||
* @param {string} format
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function formatDateTime(time = undefined, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
return dayjs(time).format(format)
|
||||
}
|
||||
|
||||
export function formatDate(date = undefined, format = 'YYYY-MM-DD') {
|
||||
return formatDateTime(date, format)
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc 函数节流
|
||||
* @param {Function} fn
|
||||
* @param {Number} wait
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function throttle(fn, wait) {
|
||||
var context, args
|
||||
var previous = 0
|
||||
|
||||
return function () {
|
||||
var now = +new Date()
|
||||
context = this
|
||||
args = arguments
|
||||
if (now - previous > wait) {
|
||||
fn.apply(context, args)
|
||||
previous = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc 函数防抖
|
||||
* @param {Function} func
|
||||
* @param {number} wait
|
||||
* @param {boolean} immediate
|
||||
* @return {*}
|
||||
*/
|
||||
export function debounce(method, wait, immediate) {
|
||||
let timeout
|
||||
return function (...args) {
|
||||
let context = this
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
// 立即执行需要两个条件,一是immediate为true,二是timeout未被赋值或被置为null
|
||||
if (immediate) {
|
||||
/**
|
||||
* 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
|
||||
* 这样确保立即执行后wait毫秒内不会被再次触发
|
||||
*/
|
||||
let callNow = !timeout
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
}, wait)
|
||||
if (callNow) {
|
||||
method.apply(context, args)
|
||||
}
|
||||
} else {
|
||||
// 如果immediate为false,则函数wait毫秒后执行
|
||||
timeout = setTimeout(() => {
|
||||
/**
|
||||
* args是一个类数组对象,所以使用fn.apply
|
||||
* 也可写作method.call(context, ...args)
|
||||
*/
|
||||
method.apply(context, args)
|
||||
}, wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
* @param {Function} cb
|
||||
* @return {ResizeObserver}
|
||||
*/
|
||||
export function useResize(el, cb) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
cb(entries[0].contentRect)
|
||||
})
|
||||
observer.observe(el)
|
||||
return observer
|
||||
}
|
||||
12
src/utils/common/icon.js
Normal file
12
src/utils/common/icon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { h } from 'vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import SvgIcon from '@/components/icon/SvgIcon.vue'
|
||||
|
||||
export function renderIcon(icon, props = { size: 12 }) {
|
||||
return () => h(NIcon, props, { default: () => h(Icon, { icon }) })
|
||||
}
|
||||
|
||||
export function renderCustomIcon(icon, props = { size: 12 }) {
|
||||
return () => h(NIcon, props, { default: () => h(SvgIcon, { icon }) })
|
||||
}
|
||||
4
src/utils/common/index.js
Normal file
4
src/utils/common/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './common'
|
||||
export * from './is'
|
||||
export * from './icon'
|
||||
export * from './naiveTools'
|
||||
119
src/utils/common/is.js
Normal file
119
src/utils/common/is.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const toString = Object.prototype.toString
|
||||
|
||||
export function is(val, type) {
|
||||
return toString.call(val) === `[object ${type}]`
|
||||
}
|
||||
|
||||
export function isDef(val) {
|
||||
return typeof val !== 'undefined'
|
||||
}
|
||||
|
||||
export function isUndef(val) {
|
||||
return typeof val === 'undefined'
|
||||
}
|
||||
|
||||
export function isNull(val) {
|
||||
return val === null
|
||||
}
|
||||
|
||||
export function isWhitespace(val) {
|
||||
return val === ''
|
||||
}
|
||||
|
||||
export function isObject(val) {
|
||||
return !isNull(val) && is(val, 'Object')
|
||||
}
|
||||
|
||||
export function isArray(val) {
|
||||
return val && Array.isArray(val)
|
||||
}
|
||||
|
||||
export function isString(val) {
|
||||
return is(val, 'String')
|
||||
}
|
||||
|
||||
export function isNumber(val) {
|
||||
return is(val, 'Number')
|
||||
}
|
||||
|
||||
export function isBoolean(val) {
|
||||
return is(val, 'Boolean')
|
||||
}
|
||||
|
||||
export function isDate(val) {
|
||||
return is(val, 'Date')
|
||||
}
|
||||
|
||||
export function isRegExp(val) {
|
||||
return is(val, 'RegExp')
|
||||
}
|
||||
|
||||
export function isFunction(val) {
|
||||
return typeof val === 'function'
|
||||
}
|
||||
|
||||
export function isPromise(val) {
|
||||
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
|
||||
}
|
||||
|
||||
export function isElement(val) {
|
||||
return isObject(val) && !!val.tagName
|
||||
}
|
||||
|
||||
export function isWindow(val) {
|
||||
return typeof window !== 'undefined' && isDef(window) && is(val, 'Window')
|
||||
}
|
||||
|
||||
export function isNullOrUndef(val) {
|
||||
return isNull(val) || isUndef(val)
|
||||
}
|
||||
|
||||
export function isNullOrWhitespace(val) {
|
||||
return isNullOrUndef(val) || isWhitespace(val)
|
||||
}
|
||||
|
||||
/** 空数组 | 空字符串 | 空对象 | 空Map | 空Set */
|
||||
export function isEmpty(val) {
|
||||
if (isArray(val) || isString(val)) {
|
||||
return val.length === 0
|
||||
}
|
||||
|
||||
if (val instanceof Map || val instanceof Set) {
|
||||
return val.size === 0
|
||||
}
|
||||
|
||||
if (isObject(val)) {
|
||||
return Object.keys(val).length === 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* * 类似mysql的IFNULL函数
|
||||
* * 第一个参数为null/undefined/'' 则返回第二个参数作为备用值,否则返回第一个参数
|
||||
* @param {Number|Boolean|String} val
|
||||
* @param {Number|Boolean|String} def
|
||||
* @returns
|
||||
*/
|
||||
export function ifNull(val, def = '') {
|
||||
return isNullOrWhitespace(val) ? def : val
|
||||
}
|
||||
|
||||
export function isUrl(path) {
|
||||
const reg =
|
||||
/(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/
|
||||
return reg.test(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
||||
|
||||
export const isServer = typeof window === 'undefined'
|
||||
|
||||
export const isClient = !isServer
|
||||
99
src/utils/common/naiveTools.js
Normal file
99
src/utils/common/naiveTools.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as NaiveUI from 'naive-ui'
|
||||
import { isNullOrUndef } from '@/utils'
|
||||
import { naiveThemeOverrides as themeOverrides } from '~/settings'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
|
||||
export function setupMessage(NMessage) {
|
||||
let loadingMessage = null
|
||||
class Message {
|
||||
/**
|
||||
* 规则:
|
||||
* * loading message只显示一个,新的message会替换正在显示的loading message
|
||||
* * loading message不会自动清除,除非被替换成非loading message,非loading message默认2秒后自动清除
|
||||
*/
|
||||
|
||||
removeMessage(message = loadingMessage, duration = 2000) {
|
||||
setTimeout(() => {
|
||||
if (message) {
|
||||
message.destroy()
|
||||
message = null
|
||||
}
|
||||
}, duration)
|
||||
}
|
||||
|
||||
showMessage(type, content, option = {}) {
|
||||
if (loadingMessage && loadingMessage.type === 'loading') {
|
||||
// 如果存在则替换正在显示的loading message
|
||||
loadingMessage.type = type
|
||||
loadingMessage.content = content
|
||||
|
||||
if (type !== 'loading') {
|
||||
// 非loading message需设置自动清除
|
||||
this.removeMessage(loadingMessage, option.duration)
|
||||
}
|
||||
} else {
|
||||
// 不存在正在显示的loading则新建一个message,如果新建的message是loading message则将message赋值存储下来
|
||||
let message = NMessage[type](content, option)
|
||||
if (type === 'loading') {
|
||||
loadingMessage = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading(content) {
|
||||
this.showMessage('loading', content, { duration: 0 })
|
||||
}
|
||||
|
||||
success(content, option = {}) {
|
||||
this.showMessage('success', content, option)
|
||||
}
|
||||
|
||||
error(content, option = {}) {
|
||||
this.showMessage('error', content, option)
|
||||
}
|
||||
|
||||
info(content, option = {}) {
|
||||
this.showMessage('info', content, option)
|
||||
}
|
||||
|
||||
warning(content, option = {}) {
|
||||
this.showMessage('warning', content, option)
|
||||
}
|
||||
}
|
||||
|
||||
return new Message()
|
||||
}
|
||||
|
||||
export function setupDialog(NDialog) {
|
||||
NDialog.confirm = function (option = {}) {
|
||||
const showIcon = !isNullOrUndef(option.title)
|
||||
return NDialog[option.type || 'warning']({
|
||||
showIcon,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: option.confirm,
|
||||
onNegativeClick: option.cancel,
|
||||
onMaskClick: option.cancel,
|
||||
...option,
|
||||
})
|
||||
}
|
||||
|
||||
return NDialog
|
||||
}
|
||||
|
||||
export function setupNaiveDiscreteApi() {
|
||||
const appStore = useAppStore()
|
||||
const configProviderProps = computed(() => ({
|
||||
theme: appStore.isDark ? NaiveUI.darkTheme : undefined,
|
||||
themeOverrides,
|
||||
}))
|
||||
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
|
||||
['message', 'dialog', 'notification', 'loadingBar'],
|
||||
{ configProviderProps }
|
||||
)
|
||||
|
||||
window.$loadingBar = loadingBar
|
||||
window.$notification = notification
|
||||
window.$message = setupMessage(message)
|
||||
window.$dialog = setupDialog(dialog)
|
||||
}
|
||||
35
src/utils/http/helpers.js
Normal file
35
src/utils/http/helpers.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
export function addBaseParams(params) {
|
||||
if (!params.userId) {
|
||||
params.userId = useUserStore().userId
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveResError(code, message) {
|
||||
switch (code) {
|
||||
case 400:
|
||||
message = message ?? '请求参数错误'
|
||||
break
|
||||
case 401:
|
||||
message = message ?? '登录已过期'
|
||||
useUserStore().logout()
|
||||
break
|
||||
case 403:
|
||||
message = message ?? '没有权限'
|
||||
break
|
||||
case 404:
|
||||
message = message ?? '资源或接口不存在'
|
||||
break
|
||||
case 500:
|
||||
message = message ?? '服务器异常'
|
||||
break
|
||||
case 402:
|
||||
message = message ?? '无权限访问'
|
||||
break
|
||||
default:
|
||||
message = message ?? `【${code}】: 未知异常!`
|
||||
break
|
||||
}
|
||||
return message
|
||||
}
|
||||
107
src/utils/http/index.js
Normal file
107
src/utils/http/index.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import axios from 'axios'
|
||||
import { resReject, resResolve, reqReject, reqResolve } from './interceptors'
|
||||
import { getCurrentEndpoint, getAvailableEndpoints } from '../api-config'
|
||||
|
||||
export function createAxios(options = {}) {
|
||||
const defaultOptions = {
|
||||
timeout: 12000,
|
||||
}
|
||||
const service = axios.create({
|
||||
...defaultOptions,
|
||||
...options,
|
||||
})
|
||||
service.interceptors.request.use(reqResolve, reqReject)
|
||||
service.interceptors.response.use(resResolve, resReject)
|
||||
return service
|
||||
}
|
||||
|
||||
// 创建支持多接口的请求实例
|
||||
export function createMultiEndpointRequest() {
|
||||
let instances = null
|
||||
|
||||
// 延迟创建axios实例
|
||||
function ensureInstances() {
|
||||
if (!instances) {
|
||||
instances = {}
|
||||
const endpoints = getAvailableEndpoints()
|
||||
|
||||
console.log('创建axios实例,接口列表:', endpoints)
|
||||
|
||||
endpoints.forEach((endpoint) => {
|
||||
console.log(`创建实例 ${endpoint.key}:`, endpoint.baseURL)
|
||||
instances[endpoint.key] = createAxios({
|
||||
baseURL: endpoint.baseURL,
|
||||
timeout: endpoint.timeout,
|
||||
})
|
||||
})
|
||||
}
|
||||
return instances
|
||||
}
|
||||
|
||||
return {
|
||||
// 使用当前选中的接口发送请求
|
||||
async request(config) {
|
||||
const instances = ensureInstances()
|
||||
const currentEndpoint = getCurrentEndpoint()
|
||||
|
||||
console.log('当前接口配置:', currentEndpoint)
|
||||
console.log('可用实例:', Object.keys(instances))
|
||||
|
||||
const instance = instances[currentEndpoint.key]
|
||||
|
||||
if (!instance) {
|
||||
throw new Error(`接口实例不存在: ${currentEndpoint.key}`)
|
||||
}
|
||||
|
||||
console.log(`使用接口: ${currentEndpoint.name} (${currentEndpoint.baseURL})`)
|
||||
console.log('请求配置:', config)
|
||||
|
||||
return await instance(config)
|
||||
},
|
||||
|
||||
// 获取当前接口实例
|
||||
getCurrentInstance() {
|
||||
const instances = ensureInstances()
|
||||
const currentEndpoint = getCurrentEndpoint()
|
||||
return instances[currentEndpoint.key]
|
||||
},
|
||||
|
||||
// 获取所有接口实例
|
||||
getAllInstances() {
|
||||
return ensureInstances()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// export const request = createAxios({
|
||||
// baseURL: import.meta.env.VITE_BASE_API,
|
||||
// })
|
||||
|
||||
// 支持多接口的请求实例
|
||||
export const multiRequest = createMultiEndpointRequest()
|
||||
|
||||
// 创建自动适配多接口的request实例
|
||||
const multiEndpointInstance = createMultiEndpointRequest()
|
||||
|
||||
// 创建支持axios方法的request实例
|
||||
export const request = {
|
||||
// 基础请求方法
|
||||
request: (config) => multiEndpointInstance.request(config),
|
||||
|
||||
// 自动适配axios方法
|
||||
get: (url, config = {}) => multiEndpointInstance.request({ method: 'get', url, ...config }),
|
||||
post: (url, data, config = {}) =>
|
||||
multiEndpointInstance.request({ method: 'post', url, data, ...config }),
|
||||
put: (url, data, config = {}) =>
|
||||
multiEndpointInstance.request({ method: 'put', url, data, ...config }),
|
||||
delete: (url, config = {}) => multiEndpointInstance.request({ method: 'delete', url, ...config }),
|
||||
patch: (url, data, config = {}) =>
|
||||
multiEndpointInstance.request({ method: 'patch', url, data, ...config }),
|
||||
head: (url, config = {}) => multiEndpointInstance.request({ method: 'head', url, ...config }),
|
||||
options: (url, config = {}) =>
|
||||
multiEndpointInstance.request({ method: 'options', url, ...config }),
|
||||
|
||||
// 获取当前接口实例(用于直接访问axios实例)
|
||||
getCurrentInstance: () => multiEndpointInstance.getCurrentInstance(),
|
||||
getAllInstances: () => multiEndpointInstance.getAllInstances(),
|
||||
}
|
||||
63
src/utils/http/interceptors.js
Normal file
63
src/utils/http/interceptors.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { getToken } from '@/utils'
|
||||
import { resolveResError } from './helpers'
|
||||
|
||||
export function reqResolve(config) {
|
||||
// if (config.url.includes('/admin')) {
|
||||
// config.baseURL = import.meta.env.VITE_ADMIN_API
|
||||
// } else {
|
||||
// config.baseURL = import.meta.env.VITE_BASE_API
|
||||
// }
|
||||
// 处理不需要token的请求
|
||||
if (config.noNeedToken) {
|
||||
return config
|
||||
}
|
||||
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
return Promise.reject({ code: 401, message: '登录已过期,请重新登录!' })
|
||||
}
|
||||
|
||||
/**
|
||||
* * 加上 token
|
||||
* ! 认证方案: JWT Bearer
|
||||
*/
|
||||
config.headers.Authorization = config.headers.Authorization || 'Bearer ' + token
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
export function reqReject(error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
export function resResolve(response) {
|
||||
// TODO: 处理不同的 response.headers
|
||||
const { data, status, config, statusText } = response
|
||||
if (data?.code !== 200) {
|
||||
const code = data?.code ?? status
|
||||
|
||||
/** 根据code处理对应的操作,并返回处理后的message */
|
||||
const message = resolveResError(code, data?.msg ?? statusText)
|
||||
|
||||
/** 需要错误提醒 */
|
||||
!config.noNeedTip && window.$message?.error(message)
|
||||
return Promise.reject({ code, message, error: data || response })
|
||||
}
|
||||
return Promise.resolve(data)
|
||||
}
|
||||
|
||||
export function resReject(error) {
|
||||
if (!error || !error.response) {
|
||||
const code = error?.code
|
||||
/** 根据code处理对应的操作,并返回处理后的message */
|
||||
const message = resolveResError(code, error.message)
|
||||
window.$message?.error(message)
|
||||
return Promise.reject({ code, message, error })
|
||||
}
|
||||
const { data, status, config } = error.response
|
||||
const code = data?.code ?? status
|
||||
const message = resolveResError(code, data?.message ?? error.message)
|
||||
/** 需要错误提醒 */
|
||||
!config?.noNeedTip && window.$message?.error(message)
|
||||
return Promise.reject({ code, message, error: error.response?.data || error.response })
|
||||
}
|
||||
4
src/utils/index.js
Normal file
4
src/utils/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './common'
|
||||
export * from './storage'
|
||||
export * from './http'
|
||||
export * from './auth'
|
||||
21
src/utils/storage/index.js
Normal file
21
src/utils/storage/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createStorage } from './storage'
|
||||
|
||||
const prefixKey = 'Vue_Naive_Admin_'
|
||||
|
||||
export const createLocalStorage = function (option = {}) {
|
||||
return createStorage({
|
||||
prefixKey: option.prefixKey || '',
|
||||
storage: localStorage,
|
||||
})
|
||||
}
|
||||
|
||||
export const createSessionStorage = function (option = {}) {
|
||||
return createStorage({
|
||||
prefixKey: option.prefixKey || '',
|
||||
storage: sessionStorage,
|
||||
})
|
||||
}
|
||||
|
||||
export const lStorage = createLocalStorage({ prefixKey })
|
||||
|
||||
export const sStorage = createSessionStorage({ prefixKey })
|
||||
55
src/utils/storage/storage.js
Normal file
55
src/utils/storage/storage.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { isNullOrUndef } from '@/utils'
|
||||
|
||||
class Storage {
|
||||
constructor(option) {
|
||||
this.storage = option.storage
|
||||
this.prefixKey = option.prefixKey
|
||||
}
|
||||
|
||||
getKey(key) {
|
||||
return `${this.prefixKey}${key}`.toUpperCase()
|
||||
}
|
||||
|
||||
set(key, value, expire) {
|
||||
const stringData = JSON.stringify({
|
||||
value,
|
||||
time: Date.now(),
|
||||
expire: !isNullOrUndef(expire) ? new Date().getTime() + expire * 1000 : null,
|
||||
})
|
||||
this.storage.setItem(this.getKey(key), stringData)
|
||||
}
|
||||
|
||||
get(key) {
|
||||
const { value } = this.getItem(key, {})
|
||||
return value
|
||||
}
|
||||
|
||||
getItem(key, def = null) {
|
||||
const val = this.storage.getItem(this.getKey(key))
|
||||
if (!val) return def
|
||||
try {
|
||||
const data = JSON.parse(val)
|
||||
const { value, time, expire } = data
|
||||
if (isNullOrUndef(expire) || expire > new Date().getTime()) {
|
||||
return { value, time }
|
||||
}
|
||||
this.remove(key)
|
||||
return def
|
||||
} catch (error) {
|
||||
this.remove(key)
|
||||
return def
|
||||
}
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
this.storage.removeItem(this.getKey(key))
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.storage.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export function createStorage({ prefixKey = '', storage = sessionStorage }) {
|
||||
return new Storage({ prefixKey, storage })
|
||||
}
|
||||
6
src/views/business/mer_class/api.js
Normal file
6
src/views/business/mer_class/api.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getList: (data) => request.post('/store/classify', data),
|
||||
addClass: (data) => request.post('/store/classify/edit', data),
|
||||
}
|
||||
261
src/views/business/mer_class/index.vue
Normal file
261
src/views/business/mer_class/index.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-button v-perms="['/admin/store/classify/edit']" type="primary" @click="handleAdd(1)">
|
||||
新增商户分类
|
||||
</n-button>
|
||||
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
remote
|
||||
:row-key="rowKey"
|
||||
children-key="Classify"
|
||||
/>
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card
|
||||
style="width: 400px"
|
||||
:title="modelTitle"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<!-- {{ formValue }} -->
|
||||
<n-form ref="formRef" label-placement="left" :model="formValue" :rules="rules">
|
||||
<n-form-item label="上级分类:" path="sid">
|
||||
<n-select
|
||||
v-model:value="formValue.sid"
|
||||
:options="options"
|
||||
placeholder="请选择上级分类"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="分类图标:" path="icon">
|
||||
<Upload v-model:list="formValue.icon" />
|
||||
</n-form-item>
|
||||
<n-form-item label="分类名称:" path="name">
|
||||
<n-input v-model:value="formValue.name" placeholder="请输入商户分类名称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="分类状态:" path="status">
|
||||
<n-switch v-model:value="formValue.status" :checked-value="1" :unchecked-value="2" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<div class="m-auto">
|
||||
<n-button
|
||||
class="m-auto"
|
||||
type="primary"
|
||||
attr-type="button"
|
||||
@click="handleValidateClick"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
<n-button class="ml-10" @click="clear">取消</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, h, withDirectives, resolveDirective } from 'vue'
|
||||
import api from './api'
|
||||
import { NButton } from 'naive-ui'
|
||||
import Upload from '@/components/Upload.vue'
|
||||
|
||||
const vPerms = resolveDirective('perms')
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const rowKey = (row) => {
|
||||
return row.Classify || []
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: 'ID',
|
||||
align: 'center',
|
||||
key: 'ID',
|
||||
},
|
||||
{
|
||||
title: '分类名称',
|
||||
align: 'center',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
slot: 'status',
|
||||
render(row) {
|
||||
return h('span', row.status === 1 ? '正常' : '禁用')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
slot: 'action',
|
||||
render(row) {
|
||||
return [
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
formValue.value = {
|
||||
...row,
|
||||
icon:
|
||||
row.icon.length > 0
|
||||
? [
|
||||
{
|
||||
url: row.icon,
|
||||
name: '图片',
|
||||
status: 'finished',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
}
|
||||
handleAdd(2)
|
||||
},
|
||||
},
|
||||
() => '编辑'
|
||||
),
|
||||
[[vPerms, ['/admin/store/classify/edit']]]
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const options = ref([])
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const formRef = ref(null)
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: true,
|
||||
message: '请输入商户分类名称',
|
||||
},
|
||||
icon: {
|
||||
required: true,
|
||||
type: 'array',
|
||||
message: '请上传分类图标',
|
||||
},
|
||||
sid: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请选择分类',
|
||||
},
|
||||
}
|
||||
|
||||
const formValue = ref({
|
||||
sid: null,
|
||||
name: '',
|
||||
status: 1,
|
||||
icon: [],
|
||||
})
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getList()
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
getList()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
getOptions()
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.getList({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
})
|
||||
data.value = res.data.data
|
||||
pagination.value.itemCount = res.data.total
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await api.getList({
|
||||
pageNum: 1,
|
||||
pageSize: 9999999999,
|
||||
})
|
||||
options.value = [
|
||||
{
|
||||
label: '顶级分类',
|
||||
value: 0,
|
||||
},
|
||||
]
|
||||
res.data.data.forEach((item) => {
|
||||
options.value.push({
|
||||
label: item.name,
|
||||
value: item.ID,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const modelTitle = ref('')
|
||||
|
||||
const handleAdd = (e) => {
|
||||
modelTitle.value = e === 1 ? '新增商户分类' : '编辑商户分类'
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
formValue.value = {
|
||||
sid: 0,
|
||||
name: '',
|
||||
status: 1,
|
||||
icon: [],
|
||||
}
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
const handleValidateClick = async (e) => {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
console.log(formValue.value)
|
||||
await api.addClass({
|
||||
...formValue.value,
|
||||
icon: formValue.value.icon[0].url,
|
||||
})
|
||||
$message.success('成功')
|
||||
clear()
|
||||
getList()
|
||||
getOptions()
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
}
|
||||
} else {
|
||||
$message.error('Invalid')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
11
src/views/business/mer_list/api.js
Normal file
11
src/views/business/mer_list/api.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getList: (data) => request.post('/store', data),
|
||||
addMer: (data) => request.post('/store/edit', data),
|
||||
getMerType: () => request.post('/store/getOther'),
|
||||
// 一键登录
|
||||
login: (data) => request.post('/store/easy/login', data),
|
||||
// 退积分
|
||||
outJf: (data) => request.post('/store/set/integral', data),
|
||||
}
|
||||
588
src/views/business/mer_list/index.vue
Normal file
588
src/views/business/mer_list/index.vue
Normal file
@@ -0,0 +1,588 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<!-- {{ formValue }} -->
|
||||
<n-button v-perms="['/store/edit']" type="primary" @click="handleAdd(1)">新增商户</n-button>
|
||||
<n-grid class="mb-10" x-gap="12" cols="6" collapsed>
|
||||
<n-gi>
|
||||
<div class="flex items-center">
|
||||
<div class="w-150">商户名称:</div>
|
||||
<n-input v-model:value="QuryVal.StoreName" type="text" placeholder="请输入商户名称" />
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<div class="flex items-center">
|
||||
<div class="w-150">商户状态:</div>
|
||||
<n-select
|
||||
v-model:value="QuryVal.Status"
|
||||
placeholder="请选择商户状态"
|
||||
clearable
|
||||
:options="[
|
||||
{
|
||||
label: '正常',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: 2,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-button type="primary" @click="getList">查询</n-button>
|
||||
<n-button class="ml10" @click="clearQuryVal">重置</n-button>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
remote
|
||||
/>
|
||||
|
||||
<n-drawer v-model:show="showModal" :width="502" placement="right">
|
||||
<n-drawer-content :title="drawerTitle" closable>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
label-placement="left"
|
||||
label-align="left"
|
||||
label-width="120px"
|
||||
:model="formValue"
|
||||
:rules="rules"
|
||||
size="medium"
|
||||
>
|
||||
<n-form-item label="商户名称:" path="name">
|
||||
<n-input
|
||||
v-model:value="formValue.name"
|
||||
:disabled="isEdit"
|
||||
placeholder="请输入商户名称"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="负责人姓名:" path="username">
|
||||
<n-input
|
||||
v-model:value="formValue.username"
|
||||
:disabled="isEdit"
|
||||
placeholder="请输入负责人姓名"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="商户手机号:" path="phone">
|
||||
<n-input
|
||||
v-model:value="formValue.phone"
|
||||
:disabled="isEdit"
|
||||
placeholder="请输入商户手机号"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="商户座机:" path="mobile">
|
||||
<n-input v-model:value="formValue.mobile" placeholder="请输入商户座机" />
|
||||
</n-form-item>
|
||||
<n-form-item label="商户地址:" path="address">
|
||||
<n-input v-model:value="formValue.address" placeholder="请输入商户地址" />
|
||||
</n-form-item>
|
||||
<n-form-item label="经营类目:" path="store_class_id">
|
||||
<n-select
|
||||
v-model:value="formValue.store_class_id"
|
||||
label-field="name"
|
||||
value-field="ID"
|
||||
clearable
|
||||
placeholder="请选择经营类目"
|
||||
:options="classOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="!isEdit" label="商户密码:" path="password">
|
||||
<n-input v-model:value="formValue.password" placeholder="请输入商户密码" />
|
||||
</n-form-item>
|
||||
<n-form-item v-else label="修改密码:" path="password">
|
||||
<n-input v-model:value="formValue.password" placeholder="不修改密码请留空" />
|
||||
</n-form-item>
|
||||
<!-- <n-form-item label="商户类型:" path="bType">-->
|
||||
<!-- <n-select-->
|
||||
<!-- v-model:value="formValue.bType"-->
|
||||
<!-- label-field="name"-->
|
||||
<!-- value-field="ID"-->
|
||||
<!-- placeholder="请选择商户类型"-->
|
||||
<!-- clearable-->
|
||||
<!-- :options="typeOptions"-->
|
||||
<!-- />-->
|
||||
<!-- </n-form-item>-->
|
||||
<n-form-item label="手续费收取类型:" path="scaleType">
|
||||
<n-select
|
||||
v-model:value="formValue.scaleType"
|
||||
placeholder="请选择手续费收取类型"
|
||||
clearable
|
||||
:options="[
|
||||
{
|
||||
label: '百分比',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '数值',
|
||||
value: 2,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="手续费比例:" path="scale">
|
||||
<n-input-number v-model:value="formValue.scale" placeholder="请输入手续费比例" />
|
||||
</n-form-item>
|
||||
<n-form-item label="提现额度:" path="withdraw_amount">
|
||||
<n-input-number
|
||||
v-model:value="formValue.withdraw_amount"
|
||||
:step="1000"
|
||||
placeholder="请输入提现额度"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="兑换额度:" path="exchange_amount">
|
||||
<n-input-number
|
||||
v-model:value="formValue.exchange_amount"
|
||||
:step="1000"
|
||||
placeholder="请输入兑换额度"
|
||||
/>
|
||||
</n-form-item>
|
||||
<!-- <n-form-item label="聚合积分额度:" path="quota">
|
||||
<n-input-number v-model:value="formValue.quota" placeholder="请输入聚合积分额度" />
|
||||
</n-form-item>
|
||||
<n-form-item label="聚合兑换比例:" path="ratio">
|
||||
<n-input-number
|
||||
v-model:value="formValue.ratio"
|
||||
placeholder="请输入聚合兑换比例"
|
||||
:precision="2"
|
||||
/>
|
||||
</n-form-item> -->
|
||||
<!-- <n-form-item label="聚合appid:" path="appid">
|
||||
<n-input v-model:value="formValue.appid" placeholder="请输入聚合appid" />
|
||||
</n-form-item>
|
||||
<n-form-item label="聚合appKey:" path="appkey">
|
||||
<n-input v-model:value="formValue.appkey" placeholder="请输入聚合appKey" />
|
||||
</n-form-item>
|
||||
<n-form-item label="聚合查询接口:" path="check_url">
|
||||
<n-input v-model:value="formValue.check_url" placeholder="请输入聚合查询接口" />
|
||||
</n-form-item>
|
||||
<n-form-item label="聚合扣除接口:" path="edit_url">
|
||||
<n-input v-model:value="formValue.edit_url" placeholder="请输入聚合扣除接口" />
|
||||
</n-form-item> -->
|
||||
<n-form-item label="商户状态:" path="status">
|
||||
<n-switch v-model:value="formValue.status" :checked-value="1" :unchecked-value="2" />
|
||||
</n-form-item>
|
||||
<n-form-item label="商户序号:" path="sort">
|
||||
<n-input-number
|
||||
v-model:value="formValue.sort"
|
||||
placeholder="请输入商户排序序号"
|
||||
:min="0"
|
||||
:max="99999999"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button
|
||||
class="m-auto w-200"
|
||||
attr-type="button"
|
||||
type="primary"
|
||||
@click="handleValidateClick"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
<!-- <n-button class="m-auto w-200" @click="handleClearValidateClick">重置</n-button> -->
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
<!-- 退积分 -->
|
||||
<n-modal v-model:show="showModalJf">
|
||||
<n-card
|
||||
style="width: 600px"
|
||||
title="退积分"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-form ref="formRefJf" :model="model" :rules="rulesJf" label-placement="left">
|
||||
<n-grid :cols="24" :x-gap="24">
|
||||
<n-form-item-gi :span="12" label="商家名称:">
|
||||
<n-input v-model:value="model.name" disabled placeholder="商家名称" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="24" label="退积分:" path="number">
|
||||
<n-input-number
|
||||
v-model:value="model.number"
|
||||
placeholder="请输入积分"
|
||||
clearable
|
||||
:precision="3"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi span="24">
|
||||
<n-button class="w-100" attr-type="button" type="primary" @click="handleOutClick">
|
||||
提交
|
||||
</n-button>
|
||||
<n-button class="ml-10 w-100" @click="handleClearOutClick">取消</n-button>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, h, withDirectives, resolveDirective } from 'vue'
|
||||
import { NButton } from 'naive-ui'
|
||||
import api from './api'
|
||||
|
||||
const vPerms = resolveDirective('perms')
|
||||
|
||||
const isEdit = computed(() => drawerTitle.value === '编辑商户')
|
||||
|
||||
const showModalJf = ref(false)
|
||||
|
||||
const formRefJf = ref(null)
|
||||
|
||||
const model = ref({
|
||||
name: '',
|
||||
bid: null,
|
||||
number: null,
|
||||
})
|
||||
|
||||
const rulesJf = ref({
|
||||
number: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入退积分',
|
||||
trigger: 'blur',
|
||||
},
|
||||
})
|
||||
|
||||
const handleOutClick = (e) => {
|
||||
e.preventDefault()
|
||||
formRefJf.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
await api.outJf({
|
||||
bid: model.value.bid,
|
||||
number: model.value.number,
|
||||
})
|
||||
$message.success('成功')
|
||||
handleClearOutClick()
|
||||
await getMertype()
|
||||
await getList()
|
||||
showModalJf.value = false
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
}
|
||||
} else {
|
||||
$message.error('Invalid')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearOutClick = () => {
|
||||
formRefJf.value?.restoreValidation()
|
||||
model.value = {
|
||||
number: null,
|
||||
}
|
||||
showModalJf.value = false
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '商户名称',
|
||||
align: 'center',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
align: 'center',
|
||||
key: 'phone',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
slot: 'status',
|
||||
render(row) {
|
||||
return h('span', row.status === 1 ? '正常' : '禁用')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '余额',
|
||||
align: 'center',
|
||||
key: 'integral',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
align: 'center',
|
||||
key: 'add_time',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
slot: 'action',
|
||||
render: (row) => {
|
||||
return [
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
text: true,
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
formValue.value = { ...row }
|
||||
Reflect.deleteProperty(formValue.value, 'password')
|
||||
handleAdd(2)
|
||||
},
|
||||
},
|
||||
() => '编辑'
|
||||
),
|
||||
[[vPerms, ['/admin/store/edit']]]
|
||||
),
|
||||
// withDirectives(
|
||||
// h(
|
||||
// NButton,
|
||||
// {
|
||||
// class: 'ml-10',
|
||||
// type: 'primary',
|
||||
// text: true,
|
||||
// size: 'small',
|
||||
// onClick: () => {
|
||||
// model.value.name = row.name
|
||||
// model.value.bid = row.bid
|
||||
// showModalJf.value = true
|
||||
// },
|
||||
// },
|
||||
// () => '退积分'
|
||||
// ),
|
||||
// [[vPerms, ['/admin/store/set/integral']]]
|
||||
// ),
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
class: 'ml-10',
|
||||
type: 'primary',
|
||||
text: true,
|
||||
size: 'small',
|
||||
onClick: async () => {
|
||||
const res = await api.login({
|
||||
bid: row.bid,
|
||||
})
|
||||
window.open(
|
||||
`${import.meta.env.VITE_MER_LOGIN_URL}?redirect=/workbench&type=${
|
||||
res.data.type
|
||||
}&tk=${res.data.token}&api=${localStorage.getItem('api_endpoint')}`
|
||||
)
|
||||
},
|
||||
},
|
||||
() => '一键登录'
|
||||
),
|
||||
[[vPerms, ['/admin/store/easy/login']]]
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const formRef = ref(null)
|
||||
|
||||
const drawerTitle = ref('新增商户')
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getList()
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
getList()
|
||||
},
|
||||
})
|
||||
|
||||
const QuryVal = ref({
|
||||
StoreName: '',
|
||||
Status: null,
|
||||
})
|
||||
|
||||
let formValue = ref({
|
||||
name: '',
|
||||
username: '',
|
||||
phone: '',
|
||||
mobile: '',
|
||||
address: '',
|
||||
store_class_id: null,
|
||||
local: '',
|
||||
password: '',
|
||||
scaleType: null,
|
||||
scale: null,
|
||||
quota: null,
|
||||
ratio: null,
|
||||
status: 2,
|
||||
sort: 0,
|
||||
withdraw_amount: 0,
|
||||
exchange_amount: 0,
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: true,
|
||||
message: '请输入商户名称',
|
||||
trigger: 'blur',
|
||||
},
|
||||
username: {
|
||||
required: true,
|
||||
message: '请输入负责人姓名',
|
||||
trigger: 'blur',
|
||||
},
|
||||
phone: {
|
||||
required: true,
|
||||
message: '请输入商户手机号',
|
||||
trigger: 'blur',
|
||||
},
|
||||
mobile: {
|
||||
required: false,
|
||||
message: '请输入商户座机',
|
||||
trigger: 'blur',
|
||||
},
|
||||
address: {
|
||||
required: true,
|
||||
message: '请输入商户地址',
|
||||
trigger: 'blur',
|
||||
},
|
||||
local: {
|
||||
required: true,
|
||||
message: '请搜索商户经纬度',
|
||||
trigger: 'blur',
|
||||
},
|
||||
store_class_id: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请选择经营类目',
|
||||
trigger: 'change',
|
||||
},
|
||||
// password: {
|
||||
// required: true,
|
||||
// message: '请输入商户密码',
|
||||
// trigger: 'blur',
|
||||
// },
|
||||
scaleType: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请选择手续费收取类型',
|
||||
trigger: 'change',
|
||||
},
|
||||
scale: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入手续费比例',
|
||||
trigger: 'blur',
|
||||
},
|
||||
withdraw_amount: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入提现额度',
|
||||
trigger: 'blur',
|
||||
},
|
||||
exchange_amount: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入兑换额度',
|
||||
trigger: 'blur',
|
||||
},
|
||||
status: {
|
||||
type: 'number',
|
||||
message: '请选择商户状态',
|
||||
trigger: 'change',
|
||||
},
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
getMertype()
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
const res = await api.getList({
|
||||
...QuryVal.value,
|
||||
PageNum: pagination.value.page,
|
||||
PageSize: pagination.value.pageSize,
|
||||
})
|
||||
data.value = res.data.data || []
|
||||
pagination.value.itemCount = res.data.total
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const classOptions = ref([])
|
||||
const typeOptions = ref([])
|
||||
|
||||
const getMertype = async () => {
|
||||
const res = await api.getMerType()
|
||||
classOptions.value = res.data.class
|
||||
typeOptions.value = res.data.type
|
||||
}
|
||||
|
||||
const clearQuryVal = () => {
|
||||
QuryVal.value = {
|
||||
StoreName: '',
|
||||
Status: null,
|
||||
}
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleAdd = (e) => {
|
||||
drawerTitle.value = e === 1 ? '新增商户' : '编辑商户'
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const handleValidateClick = (e) => {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
await api.addMer(formValue.value)
|
||||
$message.success('成功')
|
||||
handleClearValidateClick()
|
||||
await getMertype()
|
||||
await getList()
|
||||
showModal.value = false
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
}
|
||||
} else {
|
||||
$message.error('Invalid')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearValidateClick = () => {
|
||||
formRef.value?.restoreValidation()
|
||||
formValue.value = {
|
||||
name: '',
|
||||
username: '',
|
||||
phone: '',
|
||||
mobile: '',
|
||||
address: '',
|
||||
classId: null,
|
||||
local: '',
|
||||
password: '',
|
||||
bType: null,
|
||||
scaleType: null,
|
||||
scale: null,
|
||||
status: 2,
|
||||
quota: null,
|
||||
ratio: null,
|
||||
sort: 0,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
6
src/views/business/mer_type/api.js
Normal file
6
src/views/business/mer_type/api.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getMerType: (data) => request.post('/typesof', data),
|
||||
addMerType: (data) => request.post('/typesof/edit', data),
|
||||
}
|
||||
189
src/views/business/mer_type/index.vue
Normal file
189
src/views/business/mer_type/index.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-button v-perms="['/admin/typesof/edit']" type="primary" @click="handleAdd(1)">
|
||||
新增商户类型
|
||||
</n-button>
|
||||
<!-- {{ formValue }} -->
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
remote
|
||||
/>
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card
|
||||
style="width: 400px"
|
||||
:title="modelTitle"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-form ref="formRef" label-placement="left" :model="formValue" :rules="rules">
|
||||
<n-form-item label="商户类型:" path="name">
|
||||
<n-input v-model:value="formValue.name" placeholder="请输入商户类型" />
|
||||
</n-form-item>
|
||||
<n-form-item label="商户状态:" path="status">
|
||||
<n-switch v-model:value="formValue.status" :checked-value="1" :unchecked-value="2" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<div class="m-auto">
|
||||
<n-button
|
||||
class="m-auto"
|
||||
type="primary"
|
||||
attr-type="button"
|
||||
@click="handleValidateClick"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
<n-button class="ml-10" @click="clear">取消</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, h, withDirectives, resolveDirective } from 'vue'
|
||||
import api from './api'
|
||||
import { NButton } from 'naive-ui'
|
||||
const vPerms = resolveDirective('perms')
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: 'ID',
|
||||
align: 'center',
|
||||
key: 'ID',
|
||||
},
|
||||
{
|
||||
title: '商户类型',
|
||||
align: 'center',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
slot: 'status',
|
||||
render(row) {
|
||||
return h('span', row.status === 1 ? '正常' : '禁用')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
slot: 'action',
|
||||
render(row) {
|
||||
return [
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
formValue.value = row
|
||||
handleAdd(2)
|
||||
},
|
||||
},
|
||||
() => '编辑'
|
||||
),
|
||||
[[vPerms, ['/admin/typesof/edit']]]
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const formRef = ref(null)
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required: true,
|
||||
message: '请输入商户分类名称',
|
||||
},
|
||||
}
|
||||
|
||||
const formValue = ref({
|
||||
name: '',
|
||||
status: 1,
|
||||
})
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getList()
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
getList()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.getMerType({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
})
|
||||
data.value = res.data.data
|
||||
pagination.value.itemCount = res.data.total
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const modelTitle = ref('')
|
||||
|
||||
const handleAdd = (e) => {
|
||||
modelTitle.value = e === 1 ? '新增商户类型' : '编辑商户类型'
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
formValue.value = {
|
||||
name: '',
|
||||
status: 1,
|
||||
}
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
const handleValidateClick = async (e) => {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
await api.addMerType(formValue.value)
|
||||
$message.success('成功')
|
||||
clear()
|
||||
getList()
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
}
|
||||
} else {
|
||||
$message.error('Invalid')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
8
src/views/business/mer_verify/api.js
Normal file
8
src/views/business/mer_verify/api.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取入驻审核列表
|
||||
getAuditList: (data) => request.post('/process/store', data),
|
||||
// 通过审核/不通过
|
||||
passAudit: (data) => request.post('/process/store/edit', data),
|
||||
}
|
||||
211
src/views/business/mer_verify/index.vue
Normal file
211
src/views/business/mer_verify/index.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
remote
|
||||
/>
|
||||
<!-- 详情 -->
|
||||
<n-drawer v-model:show="active" :width="502" placement="right">
|
||||
<n-drawer-content title="商户入驻详情">
|
||||
<div>
|
||||
<div>商户名称:{{ nowRow.name }}</div>
|
||||
<div mt-10>用户姓名:{{ nowRow.username }}</div>
|
||||
<div mt-10>联系电话:{{ nowRow.phone }}</div>
|
||||
<div mt-10>开户行:{{ nowRow.bank }}</div>
|
||||
<div mt-10>银行卡号:{{ nowRow.bank_card }}</div>
|
||||
<!-- <div mt-10>商户类型:{{ atype.name }}</div>-->
|
||||
<div mt-10>经营类目:{{ btype.name }}</div>
|
||||
<div mt-10>
|
||||
<div>营业执照:</div>
|
||||
<n-image width="100" :src="nowRow.license" />
|
||||
</div>
|
||||
<div mt-10>
|
||||
<div>法人身份证(正面):</div>
|
||||
<n-image width="100" :src="nowRow.front" />
|
||||
</div>
|
||||
<div mt-10>
|
||||
<div>法人身份证(反面):</div>
|
||||
<n-image width="100" :src="nowRow.back" />
|
||||
</div>
|
||||
<div mt-10>
|
||||
<div>门头照:</div>
|
||||
<n-image-group>
|
||||
<n-image
|
||||
v-for="(item, index) in nowRow.img"
|
||||
:key="index"
|
||||
mr-10
|
||||
width="100"
|
||||
:src="item"
|
||||
/>
|
||||
</n-image-group>
|
||||
</div>
|
||||
</div>
|
||||
<div m-auto w-full flex justify-center>
|
||||
<n-button mr-20 type="primary" @click="ok">通过</n-button>
|
||||
<n-button mr-20 type="warning" @click="noOk">不通过</n-button>
|
||||
<n-button @click="active = false">关闭</n-button>
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { h, withDirectives, resolveDirective } from 'vue'
|
||||
import api from './api'
|
||||
import api1 from '../mer_list/api'
|
||||
import { NButton } from 'naive-ui'
|
||||
|
||||
const vPerms = resolveDirective('perms')
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const nowRow = ref({})
|
||||
|
||||
const active = ref(false)
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '商户名称',
|
||||
align: 'center',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '用户姓名',
|
||||
align: 'center',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
align: 'center',
|
||||
key: 'phone',
|
||||
},
|
||||
{
|
||||
title: '开户银行',
|
||||
align: 'center',
|
||||
key: 'bank',
|
||||
},
|
||||
{
|
||||
title: '银行卡号',
|
||||
align: 'center',
|
||||
key: 'bank_card',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
slot: 'detail',
|
||||
render: (row) => {
|
||||
return [
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
text: true,
|
||||
onClick: () => {
|
||||
nowRow.value = {
|
||||
...row,
|
||||
img: row.img.split(','),
|
||||
}
|
||||
console.log(nowRow.value)
|
||||
active.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '详情',
|
||||
}
|
||||
),
|
||||
[[vPerms, ['/admin/process/store/edit']]]
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
const data = ref([])
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
getMertype()
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
loading.value = true
|
||||
const res = await api.getAuditList({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
})
|
||||
data.value = res.data.data || []
|
||||
pagination.value.itemCount = res.data.total
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const classOptions = ref([])
|
||||
// const typeOptions = ref([])
|
||||
|
||||
const getMertype = async () => {
|
||||
const res = await api1.getMerType()
|
||||
classOptions.value = res.data.class
|
||||
// typeOptions.value = res.data.type
|
||||
}
|
||||
|
||||
// const atype = computed(() => {
|
||||
// return typeOptions.value.find((item) => {
|
||||
// if (item.ID === nowRow.value.bType) return item
|
||||
// })
|
||||
// })
|
||||
|
||||
const btype = computed(() => {
|
||||
return classOptions.value.find((item) => {
|
||||
if (item.ID === nowRow.value.store_class_id) return item
|
||||
})
|
||||
})
|
||||
|
||||
const ok = async () => {
|
||||
$dialog.warning({
|
||||
title: '提示',
|
||||
content: '同意后无法撤销,确认同意吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
const res = await api.passAudit({
|
||||
bid: nowRow.value.bid,
|
||||
status: 1,
|
||||
})
|
||||
$message.success(res.msg)
|
||||
clear()
|
||||
},
|
||||
onNegativeClick: () => {
|
||||
$message.warning('已取消操作')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const noOk = async () => {
|
||||
const res = await api.passAudit({
|
||||
bid: nowRow.value.bid,
|
||||
status: 2,
|
||||
})
|
||||
$message.success(res.msg)
|
||||
clear()
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
nowRow.value = {}
|
||||
active.value = false
|
||||
getData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
55
src/views/business/route.js
Normal file
55
src/views/business/route.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: '商户管理',
|
||||
path: '/merchant',
|
||||
component: Layout,
|
||||
redirect: '/mer_list',
|
||||
meta: {
|
||||
title: '商户管理',
|
||||
icon: 'mdi:account-multiple',
|
||||
order: 10,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'Merlist',
|
||||
path: 'mer_list',
|
||||
component: () => import('./mer_list/index.vue'),
|
||||
meta: {
|
||||
title: '商户列表',
|
||||
icon: 'mdi:account-multiple',
|
||||
order: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Classlist',
|
||||
path: 'mer_class',
|
||||
component: () => import('./mer_class/index.vue'),
|
||||
meta: {
|
||||
title: '商户分类',
|
||||
icon: 'mdi:account-multiple',
|
||||
order: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Mertype',
|
||||
path: 'mer_type',
|
||||
component: () => import('./mer_type/index.vue'),
|
||||
meta: {
|
||||
title: '商户类型',
|
||||
icon: 'mdi:account-multiple',
|
||||
order: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Merverify',
|
||||
path: 'mer_verify',
|
||||
component: () => import('./mer_verify/index.vue'),
|
||||
meta: {
|
||||
title: '入驻审核',
|
||||
icon: 'mdi:account-multiple',
|
||||
order: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
7
src/views/commodity/hot_list/api.js
Normal file
7
src/views/commodity/hot_list/api.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getHotlist: (data) => request.post('/goods', data),
|
||||
getHotStatus: (data) => request.post('/goods/process', data),
|
||||
updateType: (data) => request.post('/goods/edit/activity', data),
|
||||
}
|
||||
698
src/views/commodity/hot_list/index.vue
Normal file
698
src/views/commodity/hot_list/index.vue
Normal file
@@ -0,0 +1,698 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<!-- {{ queryParams }} -->
|
||||
<n-grid class="mb-10" x-gap="12">
|
||||
<n-gi span="12" mt-10 flex items-center>
|
||||
<span w-100>筛选条件:</span>
|
||||
<n-input-group>
|
||||
<n-select
|
||||
v-model:value="queryParams.selectKey"
|
||||
:style="{ width: '25%' }"
|
||||
:options="selectOptions"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<n-input v-model:value="queryParams.word" :style="{ width: '50%' }" />
|
||||
</n-input-group>
|
||||
</n-gi>
|
||||
<n-gi :span="24" mt-10>
|
||||
<div>
|
||||
<span>审核状态:</span>
|
||||
<n-radio-group v-model:value="queryParams.status">
|
||||
<n-radio-button
|
||||
v-for="song in [
|
||||
{
|
||||
label: '已审核',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '拒绝',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '待审核',
|
||||
value: 3,
|
||||
},
|
||||
]"
|
||||
:key="song.value"
|
||||
:value="song.value"
|
||||
:label="song.label"
|
||||
/>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi :span="24" mt-10>
|
||||
<div>
|
||||
<span>商品类型:</span>
|
||||
<n-radio-group v-model:value="queryParams.type">
|
||||
<n-radio-button
|
||||
v-for="song in [
|
||||
{
|
||||
label: '普通商品',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '活动商品',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '积分商品',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '摇球商品',
|
||||
value: 3,
|
||||
},
|
||||
]"
|
||||
:key="song.value"
|
||||
:value="song.value"
|
||||
:label="song.label"
|
||||
/>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi span="24" mt-10 flex items-center>
|
||||
<n-button type="primary" @click="getList">查询</n-button>
|
||||
<n-button ml-10 @click="clear">重置</n-button>
|
||||
</n-gi>
|
||||
<n-gi span="24" mt-10 flex items-center>
|
||||
<n-button strong secondary type="primary" @click="veeifys()">批量审核</n-button>
|
||||
<n-button strong secondary ml-10 type="primary" @click="changeGoodsType(0)">
|
||||
设为普通商品
|
||||
</n-button>
|
||||
<n-button strong secondary ml-10 type="warning" @click="changeGoodsType(1)">
|
||||
设为活动商品
|
||||
</n-button>
|
||||
<n-button strong secondary ml-10 type="info" @click="changeGoodsType(2)">
|
||||
设为兑换商品
|
||||
</n-button>
|
||||
<n-button strong secondary ml-10 type="error" @click="changeGoodsType(3)">
|
||||
设为摇球机活动商品
|
||||
</n-button>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
:row-key="(row) => row.gid"
|
||||
:checked-row-keys="queryParams.checkedRowKeysRef"
|
||||
remote
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
<!-- 拒绝 -->
|
||||
<n-modal v-model:show="isNoteModel">
|
||||
<n-card
|
||||
style="width: 500px"
|
||||
title="拒绝信息"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-input v-model:value="notesVal" type="textarea" placeholder="请输入拒绝理由...." />
|
||||
<div m-auto p-10>
|
||||
<n-button type="primary" @click="veeify">确定</n-button>
|
||||
<n-button ml-10 @click="clear">取消</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<!-- 豆子设置 -->
|
||||
<n-modal v-model:show="isDzModel">
|
||||
<n-card
|
||||
style="width: 500px"
|
||||
title="赠送/比例"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-form ref="formRef" :model="nowRow" :rules="rules" label-placement="left">
|
||||
<n-grid :cols="24">
|
||||
<n-form-item-gi :span="20" label="商品赠送豆子" path="pulse">
|
||||
<n-input-number
|
||||
v-model:value="nowRow.pulse"
|
||||
clearable
|
||||
placeholder="请输入赠送豆子数量...."
|
||||
:min="0"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="20" label="商品赠送积分" path="integral">
|
||||
<n-input-number
|
||||
v-model:value="nowRow.integral"
|
||||
clearable
|
||||
placeholder="请输入赠送积分数量...."
|
||||
:min="0"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="18" label="商品分佣类型" path="commission_type">
|
||||
<n-select
|
||||
v-model:value="nowRow.commission_type"
|
||||
placeholder="请选择分佣类型"
|
||||
clearable
|
||||
:options="[
|
||||
{
|
||||
label: '百分比',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '数值',
|
||||
value: 2,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="20" label="商品分佣比例" path="commission">
|
||||
<n-input-number
|
||||
v-model:value="nowRow.commission"
|
||||
clearable
|
||||
placeholder="请输入分佣比例...."
|
||||
:min="0"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="20" label="商品抵扣比例" path="discount">
|
||||
<n-input-number
|
||||
v-model:value="nowRow.discount"
|
||||
clearable
|
||||
placeholder="请输入抵扣比例...."
|
||||
:min="0"
|
||||
:precision="0"
|
||||
>
|
||||
<template #suffix>%</template>
|
||||
</n-input-number>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="20" label="豆子过期时间" path="expiration">
|
||||
<n-input-number
|
||||
v-model:value="nowRow.expiration"
|
||||
clearable
|
||||
placeholder="请输入豆子过期时间"
|
||||
:min="0"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12">
|
||||
<div m-auto p-10>
|
||||
<n-button type="primary" @click="veeify">确定</n-button>
|
||||
<n-button ml-10 @click="clear">取消</n-button>
|
||||
</div>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<!-- 商品详情 -->
|
||||
<n-drawer v-model:show="showDrawer" :width="502">
|
||||
<n-drawer-content title="商品详情" closable>
|
||||
<n-space vertical>
|
||||
<div>
|
||||
<span>商品分类:</span>
|
||||
<span>{{ goodInfo.class_name }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品名称:</span>
|
||||
<span>{{ goodInfo.name }}</span>
|
||||
</div>
|
||||
<div flex items-center>
|
||||
<span>封面:</span>
|
||||
<n-image width="100" :src="goodInfo.cover" />
|
||||
</div>
|
||||
<div flex items-center>
|
||||
<span>轮播图:</span>
|
||||
<div w-400 overflow-auto>
|
||||
<n-image
|
||||
v-for="(url, index) in goodInfo.rotation?.split(',')"
|
||||
:key="index"
|
||||
width="100"
|
||||
:src="url"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品价格:</span>
|
||||
<span>{{ goodInfo.number }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>市场价格:</span>
|
||||
<span>{{ goodInfo.market_num }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品库存:</span>
|
||||
<span>{{ goodInfo.stock }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品简介:</span>
|
||||
<span>{{ goodInfo.profile }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品详情:</span>
|
||||
<div v-html="goodInfo.details"></div>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from './api'
|
||||
import { NButton, NImage, NSpace, NEllipsis, NTag } from 'naive-ui'
|
||||
import { h, withDirectives, resolveDirective } from 'vue'
|
||||
|
||||
const vPerms = resolveDirective('perms')
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const isNoteModel = ref(false)
|
||||
|
||||
const isDzModel = ref(false)
|
||||
|
||||
const goodInfo = ref({})
|
||||
|
||||
const showDrawer = ref(false)
|
||||
|
||||
const notesVal = ref('')
|
||||
|
||||
const formRef = ref(null)
|
||||
|
||||
const selectOptions = ref([
|
||||
{
|
||||
label: '商品名称',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '商家名称',
|
||||
value: 1,
|
||||
},
|
||||
])
|
||||
|
||||
const queryParams = ref({
|
||||
selectKey: 0,
|
||||
word: '',
|
||||
type: '',
|
||||
status: 0,
|
||||
checkedRowKeysRef: [],
|
||||
})
|
||||
|
||||
const handleCheck = (row) => {
|
||||
queryParams.value.checkedRowKeysRef = row
|
||||
}
|
||||
|
||||
const changeGoodsType = async (type) => {
|
||||
if (queryParams.value.checkedRowKeysRef.length === 0) return $message.info('没有选中商品')
|
||||
await api.updateType({
|
||||
type: type,
|
||||
gid: queryParams.value.checkedRowKeysRef,
|
||||
})
|
||||
getList()
|
||||
queryParams.value.checkedRowKeysRef = []
|
||||
}
|
||||
|
||||
const rules = {
|
||||
pulse: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入赠送豆子数量',
|
||||
trigger: 'blur',
|
||||
},
|
||||
integral: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入赠送积分数量',
|
||||
trigger: 'blur',
|
||||
},
|
||||
commission_type: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请选择分佣类型',
|
||||
trigger: 'change',
|
||||
},
|
||||
commission: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入分佣比例',
|
||||
trigger: 'blur',
|
||||
},
|
||||
discount: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入抵扣比例',
|
||||
trigger: 'blur',
|
||||
},
|
||||
}
|
||||
|
||||
const nowRow = ref({})
|
||||
const nowKey = ref(null)
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
type: 'selection',
|
||||
},
|
||||
{
|
||||
title: '商品名称',
|
||||
slot: 'name',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h(
|
||||
NEllipsis,
|
||||
{
|
||||
style: 'max-width: 200px',
|
||||
},
|
||||
{
|
||||
default: () => row.name,
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商家名称',
|
||||
slot: 'store_name',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h(
|
||||
'span',
|
||||
{},
|
||||
{
|
||||
default: () => row.Store.name,
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商品封面',
|
||||
slot: 'cover',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
return h(NImage, {
|
||||
src: row.cover,
|
||||
width: '30',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商品分类',
|
||||
slot: 'Classify',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
{
|
||||
default: () => row.Classify.name,
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商品类型',
|
||||
slot: 'type',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
const obj = {
|
||||
0: {
|
||||
type: 'success',
|
||||
text: '普通商品',
|
||||
},
|
||||
1: {
|
||||
type: 'warning',
|
||||
text: '活动商品',
|
||||
},
|
||||
2: {
|
||||
type: 'info',
|
||||
text: '兑换商品',
|
||||
},
|
||||
3: {
|
||||
type: 'error',
|
||||
text: '摇球机活动商品',
|
||||
},
|
||||
}
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: obj[row.type].type,
|
||||
},
|
||||
{
|
||||
default: () => obj[row.type].text,
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商品价格(元)',
|
||||
key: 'number',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '抵扣后价格(元)',
|
||||
key: 'discount_price',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '积分抵扣(元)',
|
||||
key: 'exchange',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '抵扣比例(%)',
|
||||
key: 'discount',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '商品库存',
|
||||
key: 'stock',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '赠送豆子',
|
||||
key: 'pulse',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '赠送积分',
|
||||
key: 'integral',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '分佣类型',
|
||||
slot: 'commission_type',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
return row.commission_type === 1 ? '百分比' : '数值'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '分佣比例',
|
||||
key: 'commission',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '商品状态',
|
||||
slot: 'status',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
return row.status === 3 ? '待审核' : row.status === 1 ? '已审核' : '已拒绝'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'notes',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slot: 'action',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
let el = []
|
||||
if (row.status === 3) {
|
||||
el = [
|
||||
h(
|
||||
NSpace,
|
||||
{
|
||||
justify: 'center',
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
text: true,
|
||||
onClick: () => {
|
||||
nowKey.value = 1
|
||||
nowRow.value = { ...row }
|
||||
veeify()
|
||||
},
|
||||
},
|
||||
() => '审核通过'
|
||||
),
|
||||
[[vPerms, ['/admin/goods/process']]]
|
||||
),
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'error',
|
||||
text: true,
|
||||
onClick: () => {
|
||||
nowKey.value = 2
|
||||
nowRow.value = { ...row }
|
||||
isNoteModel.value = true
|
||||
},
|
||||
},
|
||||
() => '审核拒绝'
|
||||
),
|
||||
[[vPerms, ['/admin/goods/process']]]
|
||||
),
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'warning',
|
||||
text: true,
|
||||
onClick: () => {
|
||||
nowKey.value = 3
|
||||
goodInfo.value = { ...row }
|
||||
showDrawer.value = true
|
||||
},
|
||||
},
|
||||
() => '商品详情'
|
||||
),
|
||||
[[vPerms, ['/admin/goods/process']]]
|
||||
),
|
||||
],
|
||||
}
|
||||
),
|
||||
]
|
||||
} else {
|
||||
el = [
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'info',
|
||||
text: true,
|
||||
onClick: () => {
|
||||
nowRow.value = { ...row }
|
||||
nowKey.value = 3
|
||||
isDzModel.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '赠送/比例',
|
||||
}
|
||||
),
|
||||
[[vPerms, ['/admin/goods/process']]]
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
return el
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getList()
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
getList()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const query_data = {
|
||||
status: queryParams.value.status,
|
||||
type: queryParams.value.type,
|
||||
}
|
||||
|
||||
query_data[queryParams.value.selectKey === 0 ? 'name' : 'store_name'] = queryParams.value.word
|
||||
|
||||
const res = await api.getHotlist({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
...query_data,
|
||||
})
|
||||
data.value = res.data.data.sort((a, b) => b.status - a.status) || []
|
||||
pagination.value.itemCount = res.data.total
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
isNoteModel.value = false
|
||||
isDzModel.value = false
|
||||
notesVal.value = ''
|
||||
nowRow.value = {}
|
||||
queryParams.value = {
|
||||
selectKey: 0,
|
||||
word: '',
|
||||
type: '',
|
||||
status: 0,
|
||||
checkedRowKeysRef: [],
|
||||
}
|
||||
getList()
|
||||
}
|
||||
|
||||
const veeify = async () => {
|
||||
try {
|
||||
let data = {}
|
||||
if (nowKey.value === 1 || nowKey.value === 2) {
|
||||
data = {
|
||||
gid: [nowRow.value.gid],
|
||||
status: nowKey.value,
|
||||
notes: notesVal.value,
|
||||
}
|
||||
await api.getHotStatus(data)
|
||||
await getList()
|
||||
// clear()
|
||||
} else {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
data = {
|
||||
...nowRow.value,
|
||||
gid: [nowRow.value.gid],
|
||||
}
|
||||
await api.getHotStatus(data)
|
||||
await getList()
|
||||
// clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
isNoteModel.value = false
|
||||
isDzModel.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const veeifys = async () => {
|
||||
if (queryParams.value.checkedRowKeysRef.length === 0) return $message.info('没有选中商品')
|
||||
await api.getHotStatus({
|
||||
gid: queryParams.value.checkedRowKeysRef,
|
||||
status: 1,
|
||||
notes: '',
|
||||
})
|
||||
clear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
6
src/views/commodity/point/api.js
Normal file
6
src/views/commodity/point/api.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getPointlist: (data) => request.post('/point/goods', data),
|
||||
setPointStatus: (data) => request.post('/point/goods/process', data),
|
||||
}
|
||||
280
src/views/commodity/point/index.vue
Normal file
280
src/views/commodity/point/index.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
remote
|
||||
/>
|
||||
<!-- 拒绝 -->
|
||||
<n-modal v-model:show="isNoteModel">
|
||||
<n-card
|
||||
style="width: 500px"
|
||||
title="拒绝信息"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-input v-model:value="notesVal" type="textarea" placeholder="请输入拒绝理由...." />
|
||||
<div m-auto p-10>
|
||||
<n-button type="primary" @click="veeify">确定</n-button>
|
||||
<n-button ml-10 @click="clear">取消</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<!-- 商品详情 -->
|
||||
<n-drawer v-model:show="showDrawer" :width="502">
|
||||
<n-drawer-content title="商品详情" closable>
|
||||
<n-space vertical>
|
||||
<div>
|
||||
<span>商品分类:</span>
|
||||
<span>{{ goodInfo.class_name }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品名称:</span>
|
||||
<span>{{ goodInfo.name }}</span>
|
||||
</div>
|
||||
<div flex items-center>
|
||||
<span>封面:</span>
|
||||
<n-image width="100" :src="goodInfo.cover" />
|
||||
</div>
|
||||
<div flex items-center>
|
||||
<span w-90>轮播图:</span>
|
||||
<div flex flex-wrap>
|
||||
<n-image
|
||||
v-for="(url, index) in goodInfo.rotation?.split(',')"
|
||||
:key="index"
|
||||
width="100"
|
||||
:src="url"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品价格:</span>
|
||||
<span>{{ goodInfo.number }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>市场价格:</span>
|
||||
<span>{{ goodInfo.market_num }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品库存:</span>
|
||||
<span>{{ goodInfo.stock }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品简介:</span>
|
||||
<span>{{ goodInfo.profile }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品详情:</span>
|
||||
<div v-html="goodInfo.details"></div>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from './api'
|
||||
import { NEllipsis, NButton, NImage, NSpace } from 'naive-ui'
|
||||
import { h, withDirectives, resolveDirective } from 'vue'
|
||||
const vPerms = resolveDirective('perms')
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const isNoteModel = ref(false)
|
||||
|
||||
const goodInfo = ref({})
|
||||
|
||||
const showDrawer = ref(false)
|
||||
|
||||
const notesVal = ref('')
|
||||
|
||||
const nowRow = ref({})
|
||||
const nowKey = ref(null)
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '商品名称',
|
||||
slot: 'name',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h(
|
||||
NEllipsis,
|
||||
{
|
||||
style: 'max-width: 200px',
|
||||
},
|
||||
{
|
||||
default: () => row.name,
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商品封面',
|
||||
slot: 'cover',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
return h(NImage, {
|
||||
src: row.cover,
|
||||
width: '30',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商品分类',
|
||||
key: 'class_name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '商品价格',
|
||||
key: 'number',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '商品库存',
|
||||
key: 'stock',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '商品状态',
|
||||
slot: 'status',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
return row.status === 3 ? '待审核' : row.status === 1 ? '已审核' : '已拒绝'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'notes',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slot: 'action',
|
||||
align: 'center',
|
||||
render(row) {
|
||||
if (row.status === 3) {
|
||||
return [
|
||||
h(
|
||||
NSpace,
|
||||
{
|
||||
justify: 'center',
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
text: true,
|
||||
onClick: () => {
|
||||
nowKey.value = 1
|
||||
nowRow.value = { ...row }
|
||||
veeify()
|
||||
},
|
||||
},
|
||||
() => '审核通过'
|
||||
),
|
||||
[[vPerms, ['/admin/point/goods/process']]]
|
||||
),
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'error',
|
||||
text: true,
|
||||
onClick: () => {
|
||||
nowKey.value = 2
|
||||
nowRow.value = { ...row }
|
||||
isNoteModel.value = true
|
||||
},
|
||||
},
|
||||
() => '审核拒绝'
|
||||
),
|
||||
[[vPerms, ['/admin/point/goods/process']]]
|
||||
),
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'info',
|
||||
text: true,
|
||||
onClick: () => {
|
||||
nowKey.value = 3
|
||||
goodInfo.value = { ...row }
|
||||
showDrawer.value = true
|
||||
},
|
||||
},
|
||||
() => '商品详情'
|
||||
),
|
||||
[[vPerms, ['/admin/point/goods/process']]]
|
||||
),
|
||||
],
|
||||
}
|
||||
),
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getList()
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
getList()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.getPointlist({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
})
|
||||
data.value = res.data.data || []
|
||||
pagination.value.itemCount = res.data.total
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
isNoteModel.value = false
|
||||
notesVal.value = ''
|
||||
}
|
||||
|
||||
const veeify = async () => {
|
||||
await api.setPointStatus({
|
||||
gid: nowRow.value.gid,
|
||||
status: nowKey.value,
|
||||
notes: notesVal.value,
|
||||
})
|
||||
getList()
|
||||
clear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
35
src/views/commodity/route.js
Normal file
35
src/views/commodity/route.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: '商品管理',
|
||||
path: '/commodity',
|
||||
component: Layout,
|
||||
redirect: '/commodity_class',
|
||||
meta: {
|
||||
title: '商品管理',
|
||||
icon: 'mdi:account-multiple',
|
||||
order: 10,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'HotList',
|
||||
path: 'hot_list',
|
||||
component: () => import('./hot_list/index.vue'),
|
||||
meta: {
|
||||
title: '活动商品',
|
||||
icon: 'mdi:account-multiple',
|
||||
order: 10,
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'PointList',
|
||||
// path: 'point_list',
|
||||
// component: () => import('./point/index.vue'),
|
||||
// meta: {
|
||||
// title: '积分商品',
|
||||
// icon: 'mdi:account-multiple',
|
||||
// order: 10,
|
||||
// },
|
||||
// },
|
||||
],
|
||||
}
|
||||
16
src/views/error-page/404.vue
Normal file
16
src/views/error-page/404.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<AppPage>
|
||||
<n-result m-auto status="404" description="抱歉,您访问的页面不存在。">
|
||||
<template #icon>
|
||||
<img src="@/assets/images/404.webp" width="500" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<n-button @click="replace('/workbench')">返回首页</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { replace } = useRouter()
|
||||
</script>
|
||||
25
src/views/error-page/route.js
Normal file
25
src/views/error-page/route.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: 'ErrorPage',
|
||||
path: '/error-page',
|
||||
component: Layout,
|
||||
redirect: '/error-page/404',
|
||||
isHidden: true,
|
||||
meta: {
|
||||
title: '错误页',
|
||||
icon: 'mdi:alert-circle-outline',
|
||||
order: 99,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'ERROR-404',
|
||||
path: '404',
|
||||
component: () => import('./404.vue'),
|
||||
meta: {
|
||||
title: '404',
|
||||
icon: 'tabler:error-404',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
14
src/views/finance/api.js
Normal file
14
src/views/finance/api.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getData: (data) => request.post('/store/withdraw', data),
|
||||
// 审核提现
|
||||
passAudit: (data) => request.post('/store/withdraw/edit', data),
|
||||
|
||||
getYdata: (data) => request.post('/store/amount/withdraw', data),
|
||||
ydataEdit: (data) => request.post('/store/amount/withdraw/edit', data),
|
||||
// 溯源统计
|
||||
suyuanData: (data) => request.post('/pulse/count', data),
|
||||
// 获取游戏大厅
|
||||
getGameData: (data) => request.post('/game/list', data),
|
||||
}
|
||||
415
src/views/finance/index.vue
Normal file
415
src/views/finance/index.vue
Normal file
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-row gutter="12">
|
||||
<n-col :span="24">
|
||||
<div flex>
|
||||
<n-card w-400>
|
||||
<n-statistic label="总提现金额(含已驳回)" tabular-nums>
|
||||
<n-number-animation
|
||||
ref="numberAnimationInstRef"
|
||||
:from="0"
|
||||
:to="cardData.total"
|
||||
:precision="2"
|
||||
/>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
<n-card ml-10 w-400>
|
||||
<n-statistic label="服务费" tabular-nums>
|
||||
<n-number-animation
|
||||
ref="numberAnimationInstRef"
|
||||
:from="0"
|
||||
:to="cardData.commission || 0 / 100"
|
||||
:precision="2"
|
||||
/>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
<n-card ml-10 w-400>
|
||||
<n-statistic label="待处理金额" tabular-nums>
|
||||
<n-number-animation
|
||||
ref="numberAnimationInstRef"
|
||||
:from="0"
|
||||
:to="cardData.service"
|
||||
:precision="2"
|
||||
/>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
<n-card ml-10 w-400>
|
||||
<n-statistic label="已审核金额" tabular-nums>
|
||||
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.count" />
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-col>
|
||||
<n-col :span="24">
|
||||
<div mt-10>
|
||||
<span w-80>提现状态:</span>
|
||||
<n-radio-group v-model:value="queryData.status" class="ml-10">
|
||||
<n-radio-button
|
||||
v-for="song in songs"
|
||||
:key="song.value"
|
||||
:value="song.value"
|
||||
:label="song.label"
|
||||
/>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</n-col>
|
||||
<n-col :span="24">
|
||||
<div mt-10 flex items-center>
|
||||
<span w-80>号码搜索:</span>
|
||||
<n-input v-model:value="queryData.word" style="width: 25%" placeholder="请输入手机号码" />
|
||||
</div>
|
||||
</n-col>
|
||||
<n-col :span="10">
|
||||
<div mt-10 flex items-center>
|
||||
<span w-80>申请时间:</span>
|
||||
<n-date-picker
|
||||
v-model:formatted-value="queryData.time"
|
||||
value-format="yyyy-MM-dd"
|
||||
type="daterange"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</n-col>
|
||||
<n-col :span="24">
|
||||
<div mt-10>
|
||||
<n-button type="primary" @click="getList">搜索</n-button>
|
||||
<n-button ml-10 @click="clearQueryData">重置</n-button>
|
||||
</div>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-data-table
|
||||
class="mt-10"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
remote
|
||||
/>
|
||||
<!-- 打款记录 -->
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card
|
||||
style="width: 500px"
|
||||
title="打款记录"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" label-placement="left">
|
||||
<n-grid :cols="1" :x-gap="24">
|
||||
<n-form-item-gi :span="12" label="打款截图" path="img">
|
||||
<Upload v-model:list="model.img" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi>
|
||||
<div w-full flex justify-center>
|
||||
<n-button type="primary" @click="ok">确定</n-button>
|
||||
<n-button class="ml-10" @click="clear">关闭</n-button>
|
||||
</div>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from './api'
|
||||
import { NButton, NImage, NTag } from 'naive-ui'
|
||||
import Upload from '@/components/Upload.vue'
|
||||
import { h, withDirectives, resolveDirective } from 'vue'
|
||||
const vPerms = resolveDirective('perms')
|
||||
const vRole = [[vPerms, ['/admin/store/withdraw/edit']]]
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const queryData = ref({
|
||||
status: '',
|
||||
word: '',
|
||||
time: '',
|
||||
})
|
||||
|
||||
const cardData = ref({
|
||||
total: 0,
|
||||
service: 0,
|
||||
count: 0,
|
||||
commission: 0,
|
||||
})
|
||||
|
||||
const songs = ref([
|
||||
{
|
||||
value: 1,
|
||||
label: '已审核',
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: '已驳回',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: '待审核',
|
||||
},
|
||||
])
|
||||
|
||||
const nowRow = ref({})
|
||||
|
||||
const formRef = ref(null)
|
||||
|
||||
const model = ref({
|
||||
img: [],
|
||||
})
|
||||
|
||||
const rules = {
|
||||
img: { required: true, type: 'array', message: '请上传打款截图' },
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '名字',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
key: 'phone',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '银行名称',
|
||||
key: 'bank',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '银行卡号',
|
||||
key: 'bank_card',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '账户名称',
|
||||
key: 'bank_name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '法人',
|
||||
key: 'bank_user',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '提现金额',
|
||||
key: 'integral',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '上次留存余额',
|
||||
key: 'balance',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '服务费',
|
||||
key: 'commission',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '实际到账',
|
||||
key: 'number',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '剩余余额',
|
||||
key: 'residue',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '手续费比例',
|
||||
slot: 'commission_number',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h(
|
||||
'span',
|
||||
{},
|
||||
{
|
||||
default: () => `${row.commission_number}%`,
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '手续费类型',
|
||||
key: 'commission_type',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: row.commission_type === 1 ? 'success' : 'warning',
|
||||
},
|
||||
{
|
||||
default: () => (row.commission_type === 1 ? '百分比' : '固定值'),
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '审核状态',
|
||||
align: 'center',
|
||||
slot: 'status',
|
||||
render: (row) => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: row.status === 1 ? 'success' : row.status === 2 ? 'error' : 'warning',
|
||||
},
|
||||
{
|
||||
default: () => (row.status === 1 ? '已审核' : row.status === 2 ? '已驳回' : '待审核'),
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
key: 'add_time',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '打款截图',
|
||||
slot: 'img',
|
||||
render: (row) => {
|
||||
return h(NImage, {
|
||||
src: row.status_img,
|
||||
width: '50',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
slot: 'action',
|
||||
render: (row) => {
|
||||
if (row.status === 3) {
|
||||
return [
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
nowRow.value = row
|
||||
showModal.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '审核',
|
||||
}
|
||||
),
|
||||
vRole
|
||||
),
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
class: 'ml-10',
|
||||
text: true,
|
||||
type: 'error',
|
||||
onClick: () => {
|
||||
nowRow.value = row
|
||||
refuse()
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '拒绝',
|
||||
}
|
||||
),
|
||||
vRole
|
||||
),
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
const loading = ref(false)
|
||||
const data = ref([])
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getList()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
const query_data = {
|
||||
Status: queryData.value.status || '',
|
||||
Phone: queryData.value.word || '',
|
||||
StartTime: queryData.value.time === null ? '' : queryData.value.time[0] || '',
|
||||
EndTime: queryData.value.time === null ? '' : queryData.value.time[1] || '',
|
||||
}
|
||||
const res = await api.getData({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
...query_data,
|
||||
})
|
||||
|
||||
data.value = res.data.data || []
|
||||
pagination.value.itemCount = res.data.total
|
||||
cardData.value.total = res.data.all
|
||||
cardData.value.service = res.data.audit_integral
|
||||
cardData.value.commission = res.data.audit_commission
|
||||
cardData.value.count = res.data.success_integral
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const clear = async () => {
|
||||
model.value = {
|
||||
img: [],
|
||||
}
|
||||
showModal.value = false
|
||||
formRef.value?.restoreValidation()
|
||||
await getList()
|
||||
}
|
||||
|
||||
const ok = () => {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
const res = await api.passAudit({
|
||||
bid: nowRow.value.bid,
|
||||
wid: nowRow.value.wid,
|
||||
img: model.value.img[0].url,
|
||||
status: 1,
|
||||
})
|
||||
$message.success(res.msg)
|
||||
clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const refuse = async () => {
|
||||
const res = await api.passAudit({
|
||||
bid: nowRow.value.bid,
|
||||
wid: nowRow.value.wid,
|
||||
img: model.value.img[0]?.url || '',
|
||||
status: 2,
|
||||
})
|
||||
clear()
|
||||
$message.success(res.msg)
|
||||
}
|
||||
|
||||
const clearQueryData = () => {
|
||||
queryData.value = {
|
||||
status: '',
|
||||
time: null,
|
||||
word: '',
|
||||
}
|
||||
getList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
25
src/views/finance/route.js
Normal file
25
src/views/finance/route.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
|
||||
export default {
|
||||
name: '财务管理',
|
||||
path: '/finance',
|
||||
component: Layout,
|
||||
redirect: '/finance_list',
|
||||
meta: {
|
||||
title: '财务管理',
|
||||
icon: 'mdi:account-multiple',
|
||||
order: 10,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'financelist',
|
||||
path: 'finance_list',
|
||||
component: () => import('./index.vue'),
|
||||
meta: {
|
||||
title: '商户提现',
|
||||
icon: 'mdi:account-multiple',
|
||||
order: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
279
src/views/finance/suyuan.vue
Normal file
279
src/views/finance/suyuan.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-grid class="mb-10" x-gap="12">
|
||||
<n-gi :span="24">
|
||||
<div flex>
|
||||
<n-card w-250>
|
||||
<n-statistic label="总赢积分" tabular-nums>
|
||||
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.win" />
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
<n-card ml-10 w-250>
|
||||
<n-statistic label="总豆子" tabular-nums>
|
||||
<n-number-animation ref="numberAnimationInstRef" :from="0" :to="cardData.pulse" />
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi :span="12">
|
||||
<div mt-10 flex items-center>
|
||||
<div w-100>筛选条件:</div>
|
||||
<n-input-group>
|
||||
<n-select
|
||||
v-model:value="queryParams.selectKey"
|
||||
:style="{ width: '30rem' }"
|
||||
:options="selectOptions"
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<n-input v-model:value="queryParams.word" :style="{ width: '50rem' }" />
|
||||
</n-input-group>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi :span="24">
|
||||
<div mt-10 flex items-center>
|
||||
<div mr-10>游戏名称:</div>
|
||||
<n-select
|
||||
v-model:value="queryParams.hall_id"
|
||||
w-250
|
||||
:options="gamelist"
|
||||
placeholder="请选择游戏"
|
||||
/>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi :span="24" mt-10>
|
||||
<div>
|
||||
<span>豆子状态:</span>
|
||||
<n-radio-group v-model:value="queryParams.Status">
|
||||
<n-radio-button
|
||||
v-for="song in songs"
|
||||
:key="song.value"
|
||||
:value="song.value"
|
||||
:label="song.label"
|
||||
/>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi :span="24" mt-10>
|
||||
<div>
|
||||
<span>活动赠送:</span>
|
||||
<n-radio-group v-model:value="queryParams.Type">
|
||||
<n-radio-button
|
||||
v-for="song in songs1"
|
||||
:key="song.value"
|
||||
:value="song.value"
|
||||
:label="song.label"
|
||||
/>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi :span="24">
|
||||
<div mt-10 flex items-center>
|
||||
<div>时间筛选:</div>
|
||||
<n-date-picker
|
||||
v-model:formatted-value="queryParams.time"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
type="datetimerange"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi span="24" mt-10 flex items-center>
|
||||
<n-button type="primary" @click="getList">查询</n-button>
|
||||
<n-button ml-10 @click="clear">重置</n-button>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-data-table
|
||||
class="mt-10"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
remote
|
||||
/>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from './api'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const queryParams = ref({})
|
||||
|
||||
const cardData = ref({})
|
||||
|
||||
const selectOptions = [
|
||||
{
|
||||
label: '商家电话',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '用户电话',
|
||||
value: 1,
|
||||
},
|
||||
]
|
||||
|
||||
const songs = ref([
|
||||
{
|
||||
value: 1,
|
||||
label: '未使用',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: '用户赢',
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
label: '用户输',
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
label: '已过期',
|
||||
},
|
||||
])
|
||||
|
||||
const gamelist = ref([])
|
||||
|
||||
const songs1 = ref([
|
||||
{
|
||||
value: 5,
|
||||
label: '注册赠送',
|
||||
},
|
||||
{
|
||||
value: 6,
|
||||
label: '签到赠送',
|
||||
},
|
||||
{
|
||||
value: 7,
|
||||
label: '平台赠送',
|
||||
},
|
||||
])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '游戏名称',
|
||||
align: 'center',
|
||||
slot: 'game_name',
|
||||
render: (row) => {
|
||||
const res = gamelist.value.find((item) => item.value === Number(row.hall_id))
|
||||
return h('span', null, {
|
||||
default: () => res?.label || '',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '订单ID',
|
||||
align: 'center',
|
||||
key: 'order_id',
|
||||
},
|
||||
{
|
||||
title: '期数',
|
||||
align: 'center',
|
||||
key: 'periods',
|
||||
},
|
||||
{
|
||||
title: '投注豆子',
|
||||
align: 'center',
|
||||
key: 'number',
|
||||
},
|
||||
{
|
||||
title: '赢积分',
|
||||
align: 'center',
|
||||
key: 'integral',
|
||||
},
|
||||
{
|
||||
title: '用户电话',
|
||||
align: 'center',
|
||||
key: 'user_phone',
|
||||
},
|
||||
{
|
||||
title: '商户电话',
|
||||
align: 'center',
|
||||
key: 'merchant_phone',
|
||||
},
|
||||
{
|
||||
title: '投注时间',
|
||||
align: 'center',
|
||||
key: 'add_time',
|
||||
},
|
||||
{
|
||||
title: '过期时间',
|
||||
align: 'center',
|
||||
slot: 'expire',
|
||||
render: (row) => {
|
||||
return h('span', null, {
|
||||
default: () => dayjs(row.expire).format('YYYY-MM-DD HH:mm:ss'),
|
||||
})
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getList()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
getGameList()
|
||||
})
|
||||
|
||||
const getGameList = async () => {
|
||||
const res = await api.getGameData({ pageSize: 9999999, pageNum: 1 })
|
||||
gamelist.value = res.data.data.map((item) => ({ value: item.ID, label: item.name }))
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
const query_data = {
|
||||
...queryParams.value,
|
||||
StartTime: queryParams.value.time?.[0] || '',
|
||||
EndTime: queryParams.value.time?.[1] || '',
|
||||
}
|
||||
|
||||
switch (queryParams.value.selectKey) {
|
||||
case 0:
|
||||
query_data['merchant_phone'] = queryParams.value.word
|
||||
break
|
||||
case 1:
|
||||
query_data['user_phone'] = queryParams.value.word
|
||||
break
|
||||
}
|
||||
|
||||
delete query_data.time
|
||||
delete query_data.word
|
||||
const res = await api.suyuanData({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
...query_data,
|
||||
})
|
||||
data.value = res.data.result || []
|
||||
pagination.value.itemCount = res.data.count
|
||||
cardData.value.win = res.data.win
|
||||
cardData.value.pulse = res.data.pulse
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
queryParams.value = {
|
||||
word: '',
|
||||
selectKey: null,
|
||||
Status: '',
|
||||
time: null,
|
||||
Type: '',
|
||||
hall_id: '',
|
||||
}
|
||||
getList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
414
src/views/finance/yIndex.vue
Normal file
414
src/views/finance/yIndex.vue
Normal file
@@ -0,0 +1,414 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-row gutter="12">
|
||||
<n-col :span="24">
|
||||
<div flex>
|
||||
<n-card w-400>
|
||||
<n-statistic label="总提现金额(含已驳回)" tabular-nums>
|
||||
<n-number-animation
|
||||
ref="numberAnimationInstRef"
|
||||
:from="0"
|
||||
:to="cardData.total / 100"
|
||||
:precision="2"
|
||||
/>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
<n-card ml-10 w-400>
|
||||
<n-statistic label="待处理金额" tabular-nums>
|
||||
<n-number-animation
|
||||
ref="numberAnimationInstRef"
|
||||
:from="0"
|
||||
:to="cardData.service || 0 / 100"
|
||||
:precision="2"
|
||||
/>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
<n-card ml-10 w-400>
|
||||
<n-statistic label="服务费" tabular-nums>
|
||||
<n-number-animation
|
||||
ref="numberAnimationInstRef"
|
||||
:from="0"
|
||||
:to="cardData.commission || 0 / 100"
|
||||
:precision="2"
|
||||
/>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
<n-card ml-10 w-400>
|
||||
<n-statistic label="已审核金额" tabular-nums>
|
||||
<n-number-animation
|
||||
ref="numberAnimationInstRef"
|
||||
:from="0"
|
||||
:to="cardData.count || 0 / 100"
|
||||
/>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-col>
|
||||
<n-col :span="24">
|
||||
<div mt-10>
|
||||
<span w-80>提现状态:</span>
|
||||
<n-radio-group v-model:value="queryData.status" class="ml-10">
|
||||
<n-radio-button
|
||||
v-for="song in songs"
|
||||
:key="song.value"
|
||||
:value="song.value"
|
||||
:label="song.label"
|
||||
/>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</n-col>
|
||||
<n-col :span="24">
|
||||
<div mt-10 flex items-center>
|
||||
<span w-80>号码搜索:</span>
|
||||
<n-input v-model:value="queryData.word" style="width: 25%" placeholder="请输入手机号码" />
|
||||
</div>
|
||||
</n-col>
|
||||
<n-col :span="10">
|
||||
<div mt-10 flex items-center>
|
||||
<span w-80>申请时间:</span>
|
||||
<n-date-picker
|
||||
v-model:formatted-value="queryData.time"
|
||||
value-format="yyyy-MM-dd"
|
||||
type="daterange"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</n-col>
|
||||
<n-col :span="24">
|
||||
<div mt-10>
|
||||
<n-button type="primary" @click="getList">搜索</n-button>
|
||||
<n-button ml-10 @click="clearQueryData">重置</n-button>
|
||||
</div>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-data-table
|
||||
class="mt-10"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
remote
|
||||
/>
|
||||
<!-- 打款记录 -->
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card
|
||||
style="width: 500px"
|
||||
title="打款记录"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" label-placement="left">
|
||||
<n-grid :cols="1" :x-gap="24">
|
||||
<n-form-item-gi :span="12" label="打款截图" path="img">
|
||||
<Upload v-model:list="model.img" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi>
|
||||
<div w-full flex justify-center>
|
||||
<n-button type="primary" @click="ok">确定</n-button>
|
||||
<n-button class="ml-10" @click="clear">关闭</n-button>
|
||||
</div>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from './api'
|
||||
import { NButton, NImage, NTag } from 'naive-ui'
|
||||
import Upload from '@/components/Upload.vue'
|
||||
import { h, withDirectives, resolveDirective } from 'vue'
|
||||
const vPerms = resolveDirective('perms')
|
||||
const vRole = [[vPerms, ['/admin/store/amount/withdraw/edit']]]
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const queryData = ref({
|
||||
status: '',
|
||||
word: '',
|
||||
time: '',
|
||||
})
|
||||
|
||||
const cardData = ref({
|
||||
total: 0,
|
||||
service: 0,
|
||||
count: 0,
|
||||
commission: 0,
|
||||
})
|
||||
|
||||
const songs = ref([
|
||||
{
|
||||
value: 1,
|
||||
label: '已审核',
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: '已驳回',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: '待审核',
|
||||
},
|
||||
])
|
||||
|
||||
const nowRow = ref({})
|
||||
|
||||
const formRef = ref(null)
|
||||
|
||||
const model = ref({
|
||||
img: [],
|
||||
})
|
||||
|
||||
const rules = {
|
||||
img: { required: true, type: 'array', message: '请上传打款截图' },
|
||||
}
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '名字',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
key: 'phone',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '银行名称',
|
||||
key: 'bank',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '银行卡号',
|
||||
key: 'bank_card',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '账户名称',
|
||||
key: 'bank_name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '法人',
|
||||
key: 'bank_user',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '提现金额',
|
||||
key: 'integral',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '服务费',
|
||||
key: 'commission',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '实际到账',
|
||||
key: 'number',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '剩余余额',
|
||||
key: 'residue',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '手续费比例',
|
||||
slot: 'commission_number',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h(
|
||||
'span',
|
||||
{},
|
||||
{
|
||||
default: () => `${row.commission_number}%`,
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '手续费类型',
|
||||
key: 'commission_type',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: row.commission_type === 1 ? 'success' : 'warning',
|
||||
},
|
||||
{
|
||||
default: () => (row.commission_type === 1 ? '百分比' : '固定值'),
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '审核状态',
|
||||
align: 'center',
|
||||
slot: 'status',
|
||||
render: (row) => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: row.status === 1 ? 'success' : row.status === 2 ? 'error' : 'warning',
|
||||
},
|
||||
{
|
||||
default: () => (row.status === 1 ? '已审核' : row.status === 2 ? '已驳回' : '待审核'),
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '申请时间',
|
||||
key: 'add_time',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '打款截图',
|
||||
slot: 'img',
|
||||
render: (row) => {
|
||||
return h(NImage, {
|
||||
src: row.status_img,
|
||||
width: '50',
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
slot: 'action',
|
||||
render: (row) => {
|
||||
if (row.status === 3) {
|
||||
return [
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
nowRow.value = row
|
||||
showModal.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '审核',
|
||||
}
|
||||
),
|
||||
vRole
|
||||
),
|
||||
withDirectives(
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
class: 'ml-10',
|
||||
text: true,
|
||||
type: 'error',
|
||||
onClick: () => {
|
||||
nowRow.value = row
|
||||
refuse()
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => '拒绝',
|
||||
}
|
||||
),
|
||||
vRole
|
||||
),
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
])
|
||||
const loading = ref(false)
|
||||
const data = ref([])
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getList()
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
const query_data = {
|
||||
Status: queryData.value.status || '',
|
||||
Phone: queryData.value.word || '',
|
||||
StartTime: queryData.value.time === null ? '' : queryData.value.time[0] || '',
|
||||
EndTime: queryData.value.time === null ? '' : queryData.value.time[1] || '',
|
||||
}
|
||||
const res = await api.getYdata({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
...query_data,
|
||||
})
|
||||
|
||||
data.value = res.data.data || []
|
||||
pagination.value.itemCount = res.data.total
|
||||
cardData.value.total = res.data.all
|
||||
cardData.value.service = res.data.audit_number
|
||||
cardData.value.commission = res.data.audit_commission
|
||||
cardData.value.count = res.data.success_amount
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const clear = async () => {
|
||||
model.value = {
|
||||
img: [],
|
||||
}
|
||||
showModal.value = false
|
||||
formRef.value?.restoreValidation()
|
||||
await getList()
|
||||
}
|
||||
|
||||
const ok = () => {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
const res = await api.passAudit({
|
||||
bid: nowRow.value.bid,
|
||||
wid: nowRow.value.wid,
|
||||
img: model.value.img[0].url,
|
||||
status: 1,
|
||||
})
|
||||
$message.success(res.msg)
|
||||
clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const refuse = async () => {
|
||||
const res = await api.passAudit({
|
||||
bid: nowRow.value.bid,
|
||||
wid: nowRow.value.wid,
|
||||
img: model.value.img[0]?.url || '',
|
||||
status: 2,
|
||||
})
|
||||
clear()
|
||||
$message.success(res.msg)
|
||||
}
|
||||
|
||||
const clearQueryData = () => {
|
||||
queryData.value = {
|
||||
status: '',
|
||||
time: null,
|
||||
word: '',
|
||||
}
|
||||
getList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
49
src/views/game/api.js
Normal file
49
src/views/game/api.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
getData: () => request.post('/getisStart'),
|
||||
setStatus: (data) => request.post('/isStart', data),
|
||||
getDS: () => request.post('/getBetting'),
|
||||
setDS: (data) => request.post('/setBetting', data),
|
||||
getKJList: () => request.post('/draw'),
|
||||
// 获取统计
|
||||
getStatistics: (data) => request.post('/user/betting/list', data),
|
||||
|
||||
// log
|
||||
getLog: () => request.post('/log'),
|
||||
|
||||
// 宙斯详情
|
||||
getDetail: (data) => request.post('/log/betting/list', data),
|
||||
|
||||
// 获取游戏大厅
|
||||
getGameList: (data) => request.post('/game/list', data),
|
||||
|
||||
// 添加修改游戏
|
||||
addGame: (data) => request.post('/game/edit', data),
|
||||
|
||||
// 当前投注用户
|
||||
nowUser: () => request.post('/now/draw/user', {}),
|
||||
|
||||
// 全部投注用户
|
||||
allUser: () => request.post('/all/draw/user', {}),
|
||||
|
||||
// 转盘相关
|
||||
// 游戏状态
|
||||
getisBalloonStart: () => request.post('/getisTurntableStart'),
|
||||
// 修改游戏状态
|
||||
setisBalloonStart: (data) => request.post('/isTurntableStart', data),
|
||||
// 你猜
|
||||
setZsNum: () => request.post('/setTurntable'),
|
||||
// 全部开奖记录
|
||||
getBalloonList: () => request.post('/turntable/draw'),
|
||||
// 本局投注记录
|
||||
getBalloonUser: () => request.post('/now/turntable/draw/user'),
|
||||
// 全部投注记录
|
||||
getAllBalloonUser: () => request.post('/all/turntable/draw/user'),
|
||||
// 统计全部投注和中奖列表
|
||||
getTjList: () => request.post('/user/turntable/list'),
|
||||
// 宙斯记录
|
||||
getZsBalloon: (data) => request.post('/turntable/log', data),
|
||||
// 宙斯中奖
|
||||
getZsBalloonUser: (data) => request.post('/log/turntable/list', data),
|
||||
}
|
||||
348
src/views/game/balloon/data/index.vue
Normal file
348
src/views/game/balloon/data/index.vue
Normal file
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<div flex items-center>
|
||||
<div mr-20 flex>
|
||||
<div>游戏状态:</div>
|
||||
<n-switch
|
||||
v-model:value="gameStatus"
|
||||
:checked-value="1"
|
||||
:unchecked-value="2"
|
||||
@update:value="handleUpdateValue1"
|
||||
/>
|
||||
</div>
|
||||
<div ml-20 flex items-center>
|
||||
<div>本局记录:</div>
|
||||
<n-button type="primary" @click="openModal(1)">预览</n-button>
|
||||
</div>
|
||||
<div ml-20 flex items-center>
|
||||
<div>全部记录:</div>
|
||||
<n-button type="primary" @click="openModal(2)">预览</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div flex>
|
||||
<div>
|
||||
预开期数:
|
||||
<span text-25>{{ list[0]?.Periods || 0 }}</span>
|
||||
</div>
|
||||
<div ml-20>
|
||||
剩余开奖时间:
|
||||
<span text-25>{{ time || 0 }}</span>
|
||||
</div>
|
||||
<div ml-20>
|
||||
本局总下注:
|
||||
<span text-25>{{ totalA || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
第
|
||||
<span text-25>{{ data[0]?.Periods || 0 }}</span>
|
||||
期开奖结果:
|
||||
<span text-20>{{ data[0]?.Name || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<n-spin size="large" :show="show">
|
||||
<div flex flex-wrap justify-between>
|
||||
<n-card
|
||||
v-for="item in list"
|
||||
:key="item.ID"
|
||||
class="mb-10 mt-10 h-120 w-250 flex-shrink-0 cursor-pointer"
|
||||
:title="item.name"
|
||||
>
|
||||
<p text-25 op-60 :style="{ color: item.count === 0 ? 'green' : 'red' }">
|
||||
{{ item.count }}
|
||||
</p>
|
||||
</n-card>
|
||||
<div h-0 w-250></div>
|
||||
<div h-0 w-250></div>
|
||||
</div>
|
||||
</n-spin>
|
||||
<n-spin size="large" :show="show1">
|
||||
<div flex flex-wrap justify-between>
|
||||
<n-card
|
||||
v-for="item in list1"
|
||||
:key="item.ID"
|
||||
class="mb-10 mt-10 h-150 w-250 flex-shrink-0 cursor-pointer"
|
||||
:title="`${item.NumName}(${item.Name})`"
|
||||
>
|
||||
<p text-25 op-60 :style="{ color: item.Total === 0 ? 'green' : 'red' }">
|
||||
{{ item.Total }}
|
||||
</p>
|
||||
<n-popconfirm @positive-click="handleUpdateValue(item.ID)">
|
||||
<template #trigger>
|
||||
<n-button>你猜</n-button>
|
||||
</template>
|
||||
一切都将一去杳然,任何人都无法将其捕获。
|
||||
</n-popconfirm>
|
||||
</n-card>
|
||||
<div h-0 w-250></div>
|
||||
<div h-0 w-250></div>
|
||||
<div h-0 w-250></div>
|
||||
<div h-0 w-250></div>
|
||||
</div>
|
||||
</n-spin>
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card
|
||||
style="width: 900px"
|
||||
:title="modalTitle"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<template v-if="keyModal === 1">
|
||||
<div>
|
||||
<span>
|
||||
总投注(豆子):
|
||||
<span text-red>{{ nowData.total_number }}</span>
|
||||
</span>
|
||||
<span ml-20>
|
||||
总积分:
|
||||
<span text-red>{{ nowData.total_integral }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<n-data-table
|
||||
:loading="nowData.Loading"
|
||||
:columns="nowData.Columns"
|
||||
:data="nowData.data"
|
||||
:max-height="700"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
remote
|
||||
@update:sorter="nowData.handleSorterChange"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<span>
|
||||
总投注(豆子):
|
||||
<span text-red>{{ allData.total_number }}</span>
|
||||
</span>
|
||||
<span ml-20>
|
||||
总积分:
|
||||
<span text-red>{{ allData.total_integral }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<n-data-table
|
||||
:loading="allData.Loading"
|
||||
:columns="allData.Columns"
|
||||
:data="allData.data"
|
||||
:max-height="700"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
remote
|
||||
@update:sorter="allData.handleSorterChange"
|
||||
/>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from '../../api'
|
||||
import { getToken } from '@/utils'
|
||||
|
||||
const ws = new WebSocket(`wss://${import.meta.env.VITE_TRUN_WS_URL}`)
|
||||
|
||||
const ws1 = new WebSocket(`wss://${import.meta.env.VITE_TRUN_WS1_URL}`)
|
||||
|
||||
const gameStatus = ref(2)
|
||||
|
||||
const t_1 = ref()
|
||||
const t_2 = ref()
|
||||
|
||||
const show = ref(true)
|
||||
const show1 = ref(true)
|
||||
|
||||
const list = ref([])
|
||||
const list1 = ref([])
|
||||
const totalA = ref(0)
|
||||
|
||||
const time = ref(0)
|
||||
|
||||
const data = ref([])
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(t_1.value)
|
||||
clearInterval(t_2.value)
|
||||
})
|
||||
|
||||
ws.onopen = () => {
|
||||
t_1.value = setInterval(() => {
|
||||
ws.send('ping')
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
ws1.onopen = () => {
|
||||
t_2.value = setInterval(() => {
|
||||
ws1.send('ping')
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
const res = JSON.parse(e.data)
|
||||
list.value = res.betting.sort((a, b) => b.Total - a.Total)
|
||||
show.value = false
|
||||
list1.value = res.list.sort((a, b) => b.Total - a.Total)
|
||||
show1.value = false
|
||||
totalA.value = res.total
|
||||
}
|
||||
|
||||
ws1.onmessage = (e) => {
|
||||
const res = JSON.parse(e.data)
|
||||
switch (res.code) {
|
||||
case 200:
|
||||
// let num = res.data
|
||||
time.value = res.data
|
||||
break
|
||||
case 301:
|
||||
$message.error(res.msg)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
get_status()
|
||||
get_kj_jl()
|
||||
})
|
||||
|
||||
const get_kj_jl = async () => {
|
||||
try {
|
||||
const res = await api.getBalloonList()
|
||||
data.value = res.data.data
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const get_status = async () => {
|
||||
const res = await api.getisBalloonStart()
|
||||
gameStatus.value = res.data.diceStart
|
||||
}
|
||||
|
||||
const handleUpdateValue = async (e) => {
|
||||
const res = await api.setDS({
|
||||
status: 1,
|
||||
id: e,
|
||||
user_id: getToken(),
|
||||
Periods: list.value[0]?.Periods,
|
||||
})
|
||||
$message.success(res.msg)
|
||||
}
|
||||
|
||||
const handleUpdateValue1 = async (e) => {
|
||||
await api.setisBalloonStart({
|
||||
start: e,
|
||||
})
|
||||
$message.success('修改成功')
|
||||
get_status()
|
||||
}
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const keyModal = ref(null)
|
||||
|
||||
const modalTitle = ref('')
|
||||
|
||||
const openModal = (type) => {
|
||||
keyModal.value = type
|
||||
showModal.value = true
|
||||
modalTitle.value = type === 1 ? '本局记录' : '全部记录'
|
||||
if (type === 1) return fetchData(nowData)
|
||||
fetchData(allData)
|
||||
}
|
||||
|
||||
const { value: tempCol } = ref([
|
||||
{
|
||||
title: '昵称',
|
||||
key: 'User',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
key: 'Phone',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '期数',
|
||||
key: 'Periods',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '下注豆子',
|
||||
key: 'TotalCount',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
sortOrder: false,
|
||||
},
|
||||
{
|
||||
title: '赢积分',
|
||||
key: 'NumberSum',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
sortOrder: false,
|
||||
},
|
||||
{
|
||||
title: '购买选项',
|
||||
key: 'DrawTime',
|
||||
align: 'center',
|
||||
},
|
||||
])
|
||||
|
||||
const { value: nowData } = ref({
|
||||
Loading: false,
|
||||
Columns: [...tempCol],
|
||||
data: [],
|
||||
total_number: 0,
|
||||
total_integral: 0,
|
||||
handleSorterChange: (sorter) => sortData(sorter, nowData),
|
||||
api: api.getBalloonUser,
|
||||
})
|
||||
|
||||
const { value: allData } = ref({
|
||||
Loading: false,
|
||||
Columns: [...tempCol],
|
||||
data: [],
|
||||
total_number: 0,
|
||||
total_integral: 0,
|
||||
handleSorterChange: (sorter) => sortData(sorter, allData),
|
||||
api: api.getAllBalloonUser,
|
||||
})
|
||||
|
||||
const fetchData = async (target) => {
|
||||
target.Loading = true
|
||||
const res = await target.api()
|
||||
target.data = res.data.data || []
|
||||
target.total_integral = res.data.total_integral
|
||||
target.total_number = res.data.total_number
|
||||
target.Loading = false
|
||||
}
|
||||
|
||||
const sortData = (sorter, target) => {
|
||||
if (!target.Loading) {
|
||||
target.Loading = true
|
||||
switch (sorter.columnKey) {
|
||||
case 'TotalCount':
|
||||
target.Columns[3].sortOrder = !sorter ? false : sorter.order
|
||||
target.data = target.data.sort((a, b) => {
|
||||
if (sorter.order === 'descend') return b.TotalCount - a.TotalCount
|
||||
return a.TotalCount - b.TotalCount
|
||||
})
|
||||
break
|
||||
case 'NumberSum':
|
||||
target.Columns[4].sortOrder = !sorter ? false : sorter.order
|
||||
target.data = target.data.sort((a, b) => {
|
||||
if (sorter.order === 'descend') return b.NumberSum - a.NumberSum
|
||||
return a.NumberSum - b.NumberSum
|
||||
})
|
||||
break
|
||||
}
|
||||
target.Loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
214
src/views/game/balloon/statistics/index.vue
Normal file
214
src/views/game/balloon/statistics/index.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-grid class="mb-10" x-gap="12" :cols="4">
|
||||
<n-gi>
|
||||
<n-date-picker
|
||||
v-model:formatted-value="range"
|
||||
value-format="yyyy-MM-dd"
|
||||
type="daterange"
|
||||
clearable
|
||||
/>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-button type="primary" @click="getList">搜索</n-button>
|
||||
<n-button ml-10 @click="clear">重置</n-button>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<div w-full flex items-center>
|
||||
<Echarts :loading="loading" :option="option" />
|
||||
<Echarts :loading="loading" :option="option1" />
|
||||
</div>
|
||||
<div w-full flex items-center justify-between>
|
||||
<n-card title="开奖记录" :bordered="false" content-style="padding: 0;">
|
||||
<n-data-table
|
||||
:max-height="500"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:bordered="true"
|
||||
:virtual-scroll="true"
|
||||
remote
|
||||
/>
|
||||
</n-card>
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '../../api.js'
|
||||
import Echarts from '@/components/Echarts.vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const range = ref(null)
|
||||
|
||||
const option = ref({
|
||||
title: {
|
||||
text: '单期下注(豆子)/赔付(积分) 统计',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '下注(豆子)',
|
||||
data: [],
|
||||
type: 'bar',
|
||||
},
|
||||
{
|
||||
name: '赔付(积分)',
|
||||
data: [],
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const option1 = ref({
|
||||
title: {
|
||||
text: '总下注(豆子)/总赔付(积分)',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'right',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: [],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '期数',
|
||||
key: 'Periods',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖号码',
|
||||
key: 'Name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '下注',
|
||||
key: 'NumberSum',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '赔付',
|
||||
key: 'TotalCount',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
key: 'Date',
|
||||
align: 'center',
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
const clear = () => {
|
||||
range.value = null
|
||||
getList()
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
const dataObj = {
|
||||
StartTime: dayjs().format('YYYY-MM-DD'),
|
||||
EndTime: dayjs().format('YYYY-MM-DD'),
|
||||
}
|
||||
if (range.value) {
|
||||
dataObj.StartTime = range.value[0]
|
||||
dataObj.EndTime = range.value[1]
|
||||
}
|
||||
const res = await api.getTjList(dataObj)
|
||||
const newData = res.data.data || []
|
||||
data.value = newData
|
||||
option.value.xAxis.data = []
|
||||
option.value.series[0].data = []
|
||||
option.value.series[1].data = []
|
||||
option1.value.series[0].data = []
|
||||
if (newData.length > 0) {
|
||||
res.data.data.forEach((item) => {
|
||||
const a = (
|
||||
((res.data.total * 10) / (res.data.total * 10 + res.data.totalDices)) *
|
||||
100
|
||||
).toFixed(2)
|
||||
|
||||
const b = ((res.data.totalDices / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(
|
||||
2
|
||||
)
|
||||
option.value.xAxis.data.push(`第${item.Periods}期-${item.Name}`)
|
||||
option.value.series[0].name = `下注(豆子): ${a}%`
|
||||
option.value.series[0].data.push(item.NumberSum * 10)
|
||||
option.value.series[1].name = `赔付(积分): ${b}%`
|
||||
option.value.series[1].data.push(item.TotalCount)
|
||||
})
|
||||
|
||||
const a = (((res.data.total * 10) / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(
|
||||
2
|
||||
)
|
||||
const b = ((res.data.totalDices / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(2)
|
||||
|
||||
option1.value.series[0].data.push({ value: res.data.total * 10, name: `总下注: ${a}%` })
|
||||
option1.value.series[0].data.push({
|
||||
value: res.data.totalDices,
|
||||
name: `总赔付: ${b}%`,
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart {
|
||||
width: 50%;
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
189
src/views/game/balloon/zs/index.vue
Normal file
189
src/views/game/balloon/zs/index.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-data-table
|
||||
:max-height="500"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:bordered="true"
|
||||
:virtual-scroll="true"
|
||||
:pagination="pagination"
|
||||
remote
|
||||
/>
|
||||
<!-- -->
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card
|
||||
style="width: 800px"
|
||||
title="宙斯的眷顾"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-data-table
|
||||
:loading="zsLoading"
|
||||
:columns="zsColumns"
|
||||
:data="zsData"
|
||||
:pagination="zsPagination"
|
||||
:bordered="false"
|
||||
/>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from '../../api.js'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { h, ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '期数',
|
||||
key: 'periods',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖号码',
|
||||
key: 'betting_name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖数字',
|
||||
key: 'betting_number',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖时间',
|
||||
key: 'draw_time',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
key: 'add_time',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作IP',
|
||||
key: 'ip',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slot: 'action',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
strong: true,
|
||||
secondary: true,
|
||||
onClick: () => {
|
||||
openModal(row)
|
||||
},
|
||||
},
|
||||
{ default: () => '查看' }
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const data = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getData()
|
||||
},
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const res = await api.getZsBalloon({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
})
|
||||
data.value = res.data.data || []
|
||||
pagination.value.itemCount = res.data.total
|
||||
}
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const zsLoading = ref(false)
|
||||
|
||||
const zsColumns = ref([
|
||||
{
|
||||
title: '期数',
|
||||
key: 'Periods',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '数字',
|
||||
key: 'PeriodsNum',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '总投注(豆子)',
|
||||
key: 'NumberSum',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '赔付(积分)',
|
||||
key: 'TotalCount',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '选中用户',
|
||||
key: 'User',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '电话号码',
|
||||
key: 'Phone',
|
||||
align: 'center',
|
||||
},
|
||||
])
|
||||
|
||||
const zsData = ref([])
|
||||
|
||||
const zsPagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
zsPagination.value.page = page
|
||||
getData()
|
||||
},
|
||||
})
|
||||
|
||||
const openModal = async (row) => {
|
||||
showModal.value = true
|
||||
zsLoading.value = true
|
||||
const res = await api.getZsBalloonUser({
|
||||
periods: row.periods,
|
||||
draw_time: row.draw_time,
|
||||
pageNum: zsPagination.value.page,
|
||||
pageSize: zsPagination.value.pageSize,
|
||||
})
|
||||
zsData.value = res.data.data || []
|
||||
zsPagination.value.itemCount = res.data.total
|
||||
zsLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
384
src/views/game/dice/data/index.vue
Normal file
384
src/views/game/dice/data/index.vue
Normal file
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<div flex items-center justify-between>
|
||||
<div mr-20 flex>
|
||||
<div>游戏状态:</div>
|
||||
<n-switch
|
||||
v-model:value="val1"
|
||||
:checked-value="1"
|
||||
:unchecked-value="2"
|
||||
@update:value="handleUpdateValue1"
|
||||
/>
|
||||
</div>
|
||||
<div flex items-center>
|
||||
<div ml-20 flex items-center>
|
||||
<div>开奖记录:</div>
|
||||
<n-button type="primary" @click="openData">预览</n-button>
|
||||
</div>
|
||||
<div ml-20 flex items-center>
|
||||
<div>本局记录:</div>
|
||||
<n-button type="primary" @click="openJl(1)">预览</n-button>
|
||||
</div>
|
||||
<div ml-20 flex items-center>
|
||||
<div>全部记录:</div>
|
||||
<n-button type="primary" @click="openJl(2)">预览</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div flex>
|
||||
<div>
|
||||
预开期数:
|
||||
<span text-25>{{ list[0]?.Periods || 0 }}</span>
|
||||
</div>
|
||||
<div ml-20>
|
||||
剩余开奖时间:
|
||||
<span text-25>{{ time || 0 }}</span>
|
||||
</div>
|
||||
<div ml-20>
|
||||
本局总下注:
|
||||
<span text-25>{{ totalA || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
第
|
||||
<span text-25>{{ data[0]?.Periods || 0 }}</span>
|
||||
期开奖结果:
|
||||
<span text-20>{{ data[0]?.Name || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-spin size="large" :show="show">
|
||||
<div flex flex-wrap justify-between>
|
||||
<n-card
|
||||
v-for="item in list"
|
||||
:key="item.ID"
|
||||
class="mb-10 mt-10 h-120 w-250 flex-shrink-0 cursor-pointer"
|
||||
:title="item.name"
|
||||
>
|
||||
<p text-25 op-60 :style="{ color: item.count === 0 ? 'green' : 'red' }">
|
||||
{{ item.count }}
|
||||
</p>
|
||||
</n-card>
|
||||
<div h-0 w-250></div>
|
||||
<div h-0 w-250></div>
|
||||
</div>
|
||||
</n-spin>
|
||||
<n-spin size="large" :show="show1">
|
||||
<div flex flex-wrap justify-between>
|
||||
<n-card
|
||||
v-for="item in list1"
|
||||
:key="item.ID"
|
||||
class="mb-10 mt-10 h-150 w-250 flex-shrink-0 cursor-pointer"
|
||||
:title="`${item.NumName}(${item.Name})`"
|
||||
>
|
||||
<p text-25 op-60 :style="{ color: item.Total === 0 ? 'green' : 'red' }">
|
||||
{{ item.Total }}
|
||||
</p>
|
||||
<n-popconfirm @positive-click="handleUpdateValue(item.ID)">
|
||||
<template #trigger>
|
||||
<n-button>你猜</n-button>
|
||||
</template>
|
||||
一切都将一去杳然,任何人都无法将其捕获。
|
||||
</n-popconfirm>
|
||||
</n-card>
|
||||
<div h-0 w-250></div>
|
||||
<div h-0 w-250></div>
|
||||
<div h-0 w-250></div>
|
||||
<div h-0 w-250></div>
|
||||
</div>
|
||||
</n-spin>
|
||||
<!-- 开奖记录 -->
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card
|
||||
style="width: 900px"
|
||||
title="开奖记录"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-data-table
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:max-height="600"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
remote
|
||||
/>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<!-- 游戏记录 -->
|
||||
<n-modal v-model:show="jlModal">
|
||||
<n-card
|
||||
style="width: 900px"
|
||||
:title="jlTitle"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
总投注(豆子):
|
||||
<span text-red>{{ jlData.total_number }}</span>
|
||||
</span>
|
||||
<span ml-20>
|
||||
总积分:
|
||||
<span text-red>{{ jlData.total_integral }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<n-data-table
|
||||
:loading="jlLoading"
|
||||
:columns="jlColumns"
|
||||
:data="jlData.data"
|
||||
:max-height="600"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
remote
|
||||
@update:sorter="handleSorterChange"
|
||||
/>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { h } from 'vue'
|
||||
import api from '../../api'
|
||||
import { getToken } from '@/utils'
|
||||
|
||||
const ws = new WebSocket(`wss://${import.meta.env.VITE_WS_URL}`)
|
||||
|
||||
const ws1 = new WebSocket(`wss://${import.meta.env.VITE_WS1_URL}`)
|
||||
|
||||
const list = ref([])
|
||||
const list1 = ref([])
|
||||
|
||||
const val1 = ref(null)
|
||||
// const val = ref(null)
|
||||
|
||||
const time = ref(null)
|
||||
|
||||
watch(time, (val) => {
|
||||
if (Number(val) === 0) {
|
||||
setTimeout(() => {
|
||||
get_kj_jl()
|
||||
}, 2500)
|
||||
}
|
||||
})
|
||||
|
||||
const show = ref(true)
|
||||
const show1 = ref(true)
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('1连接成功')
|
||||
setInterval(() => {
|
||||
ws.send('ping')
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
const totalA = ref(null)
|
||||
|
||||
ws.onmessage = (msg) => {
|
||||
const res = JSON.parse(msg.data)
|
||||
list.value = res.betting.sort((a, b) => b.Total - a.Total)
|
||||
show.value = false
|
||||
list1.value = res.list.sort((a, b) => b.Total - a.Total)
|
||||
show1.value = false
|
||||
totalA.value = res.total
|
||||
}
|
||||
|
||||
ws1.onopen = () => {
|
||||
console.log('2连接成功')
|
||||
setInterval(() => {
|
||||
ws.send('ping')
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
ws1.onmessage = (msg) => {
|
||||
const res = JSON.parse(msg.data)
|
||||
switch (res.code) {
|
||||
case 200:
|
||||
// let num = res.data
|
||||
time.value = res.data
|
||||
break
|
||||
case 301:
|
||||
$message.error(res.msg)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// get_data()
|
||||
get_data1()
|
||||
get_kj_jl()
|
||||
})
|
||||
|
||||
// const get_data = async () => {
|
||||
// const res = await api.getDS()
|
||||
// val.value = res.data.diceStatus
|
||||
// }
|
||||
const get_data1 = async () => {
|
||||
const res = await api.getData()
|
||||
val1.value = res.data.diceStart
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ws.close()
|
||||
})
|
||||
|
||||
const handleUpdateValue = async (e) => {
|
||||
const res = await api.setDS({
|
||||
status: 1,
|
||||
id: e,
|
||||
user_id: getToken(),
|
||||
Periods: list.value[0]?.Periods,
|
||||
})
|
||||
$message.success(res.msg)
|
||||
// get_data()
|
||||
}
|
||||
|
||||
const handleUpdateValue1 = async (e) => {
|
||||
const res = await api.setStatus({
|
||||
start: e,
|
||||
})
|
||||
$message.success(res.msg)
|
||||
get_data1()
|
||||
}
|
||||
|
||||
const showModal = ref(false)
|
||||
const loading = ref(false)
|
||||
const columns = ref([
|
||||
{
|
||||
title: '期数',
|
||||
key: 'Periods',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖号码',
|
||||
key: 'Name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖时间',
|
||||
key: 'DrawTime',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖号码',
|
||||
slot: 'Num',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return h('p', `${row.Start}-${row.End}`)
|
||||
},
|
||||
},
|
||||
])
|
||||
const data = ref([])
|
||||
const openData = () => {
|
||||
showModal.value = true
|
||||
get_kj_jl()
|
||||
}
|
||||
const get_kj_jl = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await api.getKJList()
|
||||
console.log(res)
|
||||
data.value = res.data.data
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const jlModal = ref(false)
|
||||
const jlTitle = ref('')
|
||||
|
||||
const jlData = ref({})
|
||||
|
||||
const jlColumns = ref([
|
||||
{
|
||||
title: '昵称',
|
||||
key: 'User',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
key: 'Phone',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '期数',
|
||||
key: 'Periods',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '下注豆子',
|
||||
key: 'TotalCount',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
sortOrder: false,
|
||||
},
|
||||
{
|
||||
title: '赢积分',
|
||||
key: 'NumberSum',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
sortOrder: false,
|
||||
},
|
||||
{
|
||||
title: '购买号码',
|
||||
key: 'PeriodsNum',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
key: 'DrawTime',
|
||||
align: 'center',
|
||||
},
|
||||
])
|
||||
|
||||
const jlLoading = ref(false)
|
||||
|
||||
const openJl = async (e) => {
|
||||
try {
|
||||
jlModal.value = true
|
||||
jlTitle.value = e === 1 ? '本局游戏记录' : '全部游戏记录'
|
||||
jlLoading.value = true
|
||||
const res = e === 1 ? await api.nowUser() : await api.allUser()
|
||||
jlData.value = res.data
|
||||
} catch (error) {
|
||||
$message.error(error.msg)
|
||||
}
|
||||
jlLoading.value = false
|
||||
}
|
||||
|
||||
const handleSorterChange = (sorter) => {
|
||||
if (!jlLoading.value) {
|
||||
jlLoading.value = true
|
||||
switch (sorter.columnKey) {
|
||||
case 'TotalCount':
|
||||
jlColumns.value[3].sortOrder = !sorter ? false : sorter.order
|
||||
jlData.value.data = jlData.value.data.sort((a, b) => {
|
||||
if (sorter.order === 'descend') return b.TotalCount - a.TotalCount
|
||||
return a.TotalCount - b.TotalCount
|
||||
})
|
||||
break
|
||||
case 'NumberSum':
|
||||
jlColumns.value[4].sortOrder = !sorter ? false : sorter.order
|
||||
jlData.value.data = jlData.value.data.sort((a, b) => {
|
||||
if (sorter.order === 'descend') return b.NumberSum - a.NumberSum
|
||||
return a.NumberSum - b.NumberSum
|
||||
})
|
||||
break
|
||||
}
|
||||
jlLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
214
src/views/game/dice/statistics/index.vue
Normal file
214
src/views/game/dice/statistics/index.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-grid class="mb-10" x-gap="12" :cols="4">
|
||||
<n-gi>
|
||||
<n-date-picker
|
||||
v-model:formatted-value="range"
|
||||
value-format="yyyy-MM-dd"
|
||||
type="daterange"
|
||||
clearable
|
||||
/>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-button type="primary" @click="getList">搜索</n-button>
|
||||
<n-button ml-10 @click="clear">重置</n-button>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<div w-full flex items-center>
|
||||
<Echarts :loading="loading" :option="option" />
|
||||
<Echarts :loading="loading" :option="option1" />
|
||||
</div>
|
||||
<div w-full flex items-center justify-between>
|
||||
<n-card title="开奖记录" :bordered="false" content-style="padding: 0;">
|
||||
<n-data-table
|
||||
:max-height="500"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:bordered="true"
|
||||
:virtual-scroll="true"
|
||||
remote
|
||||
/>
|
||||
</n-card>
|
||||
</div>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '../../api.js'
|
||||
import Echarts from '@/components/Echarts.vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const range = ref(null)
|
||||
|
||||
const option = ref({
|
||||
title: {
|
||||
text: '单期下注(豆子)/赔付(积分) 统计',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '下注(豆子)',
|
||||
data: [],
|
||||
type: 'bar',
|
||||
},
|
||||
{
|
||||
name: '赔付(积分)',
|
||||
data: [],
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const option1 = ref({
|
||||
title: {
|
||||
text: '总下注(豆子)/总赔付(积分)',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'right',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: [],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '期数',
|
||||
key: 'Periods',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '购买号码',
|
||||
key: 'Name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '下注',
|
||||
key: 'NumberSum',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '赔付',
|
||||
key: 'TotalCount',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
key: 'Date',
|
||||
align: 'center',
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
const clear = () => {
|
||||
range.value = null
|
||||
getList()
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
const dataObj = {
|
||||
StartTime: dayjs().format('YYYY-MM-DD'),
|
||||
EndTime: dayjs().format('YYYY-MM-DD'),
|
||||
}
|
||||
if (range.value) {
|
||||
dataObj.StartTime = range.value[0]
|
||||
dataObj.EndTime = range.value[1]
|
||||
}
|
||||
const res = await api.getStatistics(dataObj)
|
||||
const newData = res.data.data || []
|
||||
data.value = newData
|
||||
option.value.xAxis.data = []
|
||||
option.value.series[0].data = []
|
||||
option.value.series[1].data = []
|
||||
option1.value.series[0].data = []
|
||||
if (newData.length > 0) {
|
||||
res.data.data.forEach((item) => {
|
||||
const a = (
|
||||
((res.data.total * 10) / (res.data.total * 10 + res.data.totalDices)) *
|
||||
100
|
||||
).toFixed(2)
|
||||
|
||||
const b = ((res.data.totalDices / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(
|
||||
2
|
||||
)
|
||||
option.value.xAxis.data.push(`第${item.Periods}期-${item.Name}`)
|
||||
option.value.series[0].name = `下注(豆子): ${a}%`
|
||||
option.value.series[0].data.push(item.NumberSum * 10)
|
||||
option.value.series[1].name = `赔付(积分): ${b}%`
|
||||
option.value.series[1].data.push(item.TotalCount)
|
||||
})
|
||||
|
||||
const a = (((res.data.total * 10) / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(
|
||||
2
|
||||
)
|
||||
const b = ((res.data.totalDices / (res.data.total * 10 + res.data.totalDices)) * 100).toFixed(2)
|
||||
|
||||
option1.value.series[0].data.push({ value: res.data.total * 10, name: `总下注: ${a}%` })
|
||||
option1.value.series[0].data.push({
|
||||
value: res.data.totalDices,
|
||||
name: `总赔付: ${b}%`,
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart {
|
||||
width: 50%;
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
189
src/views/game/dice/zs/index.vue
Normal file
189
src/views/game/dice/zs/index.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<CommonPage show-footer :title="$route.title">
|
||||
<n-data-table
|
||||
:max-height="500"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:bordered="true"
|
||||
:virtual-scroll="true"
|
||||
:pagination="pagination"
|
||||
remote
|
||||
/>
|
||||
<!-- -->
|
||||
<n-modal v-model:show="showModal">
|
||||
<n-card
|
||||
style="width: 800px"
|
||||
title="宙斯的眷顾"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<n-data-table
|
||||
:loading="zsLoading"
|
||||
:columns="zsColumns"
|
||||
:data="zsData"
|
||||
:pagination="zsPagination"
|
||||
:bordered="false"
|
||||
/>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import api from '../../api.js'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { h, ref, onMounted } from 'vue'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const columns = ref([
|
||||
{
|
||||
title: '期数',
|
||||
key: 'periods',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖号码',
|
||||
key: 'betting_name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖数字',
|
||||
key: 'betting_number',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
key: 'name',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '开奖时间',
|
||||
key: 'draw_time',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
key: 'add_time',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作IP',
|
||||
key: 'ip',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slot: 'action',
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
strong: true,
|
||||
secondary: true,
|
||||
onClick: () => {
|
||||
openModal(row)
|
||||
},
|
||||
},
|
||||
{ default: () => '查看' }
|
||||
),
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const data = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
pagination.value.page = page
|
||||
getData()
|
||||
},
|
||||
})
|
||||
|
||||
const getData = async () => {
|
||||
const res = await api.getLog({
|
||||
pageNum: pagination.value.page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
})
|
||||
data.value = res.data.data || []
|
||||
pagination.value.itemCount = res.data.total
|
||||
}
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const zsLoading = ref(false)
|
||||
|
||||
const zsColumns = ref([
|
||||
{
|
||||
title: '期数',
|
||||
key: 'Periods',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '数字',
|
||||
key: 'PeriodsNum',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '总投注(豆子)',
|
||||
key: 'NumberSum',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '赔付(积分)',
|
||||
key: 'TotalCount',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '选中用户',
|
||||
key: 'User',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '电话号码',
|
||||
key: 'Phone',
|
||||
align: 'center',
|
||||
},
|
||||
])
|
||||
|
||||
const zsData = ref([])
|
||||
|
||||
const zsPagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page) => {
|
||||
zsPagination.value.page = page
|
||||
getData()
|
||||
},
|
||||
})
|
||||
|
||||
const openModal = async (row) => {
|
||||
showModal.value = true
|
||||
zsLoading.value = true
|
||||
const res = await api.getDetail({
|
||||
periods: row.periods,
|
||||
draw_time: row.draw_time,
|
||||
pageNum: zsPagination.value.page,
|
||||
pageSize: zsPagination.value.pageSize,
|
||||
})
|
||||
zsData.value = res.data.data || []
|
||||
zsPagination.value.itemCount = res.data.total
|
||||
zsLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user