add advertising components

This commit is contained in:
zhangtao 2022-11-21 09:32:31 +08:00
parent 8dc03fe6ea
commit 500c1ddb15
28 changed files with 1011 additions and 181 deletions

5
.env
View File

@ -5,4 +5,7 @@ VITE_APP_TITLE = '智慧河长'
VITE_PORT = 3000 VITE_PORT = 3000
VITE_SERVER = "/" # VITE_SERVER = "/hhz/admin/api"
VITE_SERVER = ""
VITE_PLATFORM = "tuoheng-hhz-admin"

View File

@ -9,3 +9,8 @@ VITE_PROXY = [["/api","http://192.168.11.11:9011/api"]]
# base api # base api
VITE_APP_GLOB_BASE_API = '/api' VITE_APP_GLOB_BASE_API = '/api'
VITE_AUTHORITY = 'http://192.168.11.11:8090'
VITE_CLIENT_ID = 'tuoheng-hhz-admin'
VITE_CLIENT_SECRET = 'qsPaU8a2YGFsZfIa7HoGSz=='
VITE_REDIRECT_URI = 'http://192.168.11.11:8086/login'

View File

@ -12,3 +12,8 @@ VITE_APP_GLOB_BASE_API = '/api-local'
# mock base api # mock base api
VITE_APP_GLOB_BASE_API_MOCK = '/api-mock' VITE_APP_GLOB_BASE_API_MOCK = '/api-mock'
VITE_AUTHORITY = 'http://192.168.11.11:8090'
VITE_CLIENT_ID = 'tuoheng-hhz-admin'
VITE_CLIENT_SECRET = 'qsPaU8a2YGFsZfIa7HoGSz=='
VITE_REDIRECT_URI = 'http://192.168.12.8:3000/login'

View File

@ -9,3 +9,8 @@ VITE_PROXY = [["/api","http://127.0.0.1:8002/api"]]
# base api # base api
VITE_APP_GLOB_BASE_API = '/api' VITE_APP_GLOB_BASE_API = '/api'
VITE_AUTHORITY = 'https://oidc.t-aaron.com'
VITE_CLIENT_ID = 'tuoheng-hhz-admin'
VITE_CLIENT_SECRET = 'qsPaU8a2YGFsZfIa7HoGSz=='
VITE_REDIRECT_URI = 'https://dsp-portal.t-aaron.com/login'

View File

@ -9,3 +9,8 @@ VITE_PROXY = [["/api","http://192.168.11.241:9011/api"]]
# base api # base api
VITE_APP_GLOB_BASE_API = '/api' VITE_APP_GLOB_BASE_API = '/api'
VITE_AUTHORITY = 'https://oidc.test.t-aaron.com'
VITE_CLIENT_ID = 'tuoheng-hhz-admin'
VITE_CLIENT_SECRET = 'qsPaU8a2YGFsZfIa7HoGSz=='
VITE_REDIRECT_URI = 'http://192.168.11.241:8086/login'

View File

@ -16,6 +16,7 @@
"axios": "^0.26.1", "axios": "^0.26.1",
"dayjs": "^1.11.2", "dayjs": "^1.11.2",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"oidc-client": "^1.11.5",
"pinia": "^2.0.13", "pinia": "^2.0.13",
"pinia-plugin-persist": "^1.0.0", "pinia-plugin-persist": "^1.0.0",
"tinymce": "^5.10.2", "tinymce": "^5.10.2",

View File

@ -15,6 +15,40 @@ export const refreshToken = () => {
}) })
} }
/**
* @description: 获取menu和permission
* @param {String} roleId 角色Id
* @return {*}
*/
export const fetchPermission = (roleId) => {
return request({
url: `/permission/getRolePermission/${roleId}`,
method: 'GET'
})
}
/**
* @description: 测试接口one
* @return {*}
*/
export const permissionOne = () => {
return request({
url: 'permission/getOne',
method: 'GET'
})
}
/**
* @description: 测试接口two
* @return {*}
*/
export const permissionTwo = (id) => {
return request({
url: 'permission/getTwo',
method: 'GET'
})
}
export function getMenu() { export function getMenu() {
return request({ return request({
url: '/index/getMenuList', url: '/index/getMenuList',

View File

@ -0,0 +1,48 @@
import { defAxios as request } from '@/utils/http'
/**
* @description: 分页查询广告列表
* @return {*}
*/
export function fetchAdList(params) {
return request({
url: '/ad/index',
method: 'get',
params
})
}
/**
* @description: 创建广告
* @return {*}
*/
export function advertisingCreate(data) {
return request({
url: '/ad/add',
method: 'post',
data
})
}
/**
* @description: 更新广告信息
* @return {*}
*/
export function advertisingUpdate(data) {
return request({
url: `/ad/edit`,
method: 'put',
data
})
}
/**
* @description: 删除广告
* @return {*}
*/
export function advertisingDelete(ids) {
return request({
url: `/ad/delete/${ids}`,
method: 'delete'
})
}

View File

@ -8,13 +8,6 @@ export function getUsers(data = {}) {
}) })
} }
export function getUser() {
return request({
url: '/index/getUserInfo',
method: 'get'
})
}
export function saveUser(data = {}, id) { export function saveUser(data = {}, id) {
if (id) { if (id) {
return request({ return request({

View File

@ -76,9 +76,10 @@ export default defineComponent({
/* 获取tags的属性 */ /* 获取tags的属性 */
const getProps = computed(() => { const getProps = computed(() => {
return { return {
...unref(props.tags),
closable: false, closable: false,
bordered: props.tags?.bordered || false bordered: false,
color: { color: 'transparent' },
...unref(props.tags)
} }
}) })
return { return {
@ -93,7 +94,7 @@ export default defineComponent({
</script> </script>
<style scoped lang='scss'> <style scoped lang='scss'>
.n-tag{ // .n-tag{
background: transparent; // background: transparent;
} // }
</style> </style>

View File

@ -141,7 +141,6 @@ export default {
watch( watch(
() => props.modelValue, () => props.modelValue,
(value) => { (value) => {
console.log(value)
content.value = value content.value = value
} }
) )
@ -162,7 +161,7 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.tox,.tox-tinymce-aux{ // .tox,.tox-tinymce-aux{
z-index: 3000 !important; // z-index: 3000 !important;
} // }
</style> </style>

View File

@ -24,6 +24,7 @@ import { EditOutlined, LogoutOutlined } from '@vicons/antd'
import { renderIcon } from '@/utils' import { renderIcon } from '@/utils'
import { useUserStore } from '@/store/modules/user.js' import { useUserStore } from '@/store/modules/user.js'
import { useSettingStore } from '@/store/modules/setting.js' import { useSettingStore } from '@/store/modules/setting.js'
import { signoutRedirect } from '@/utils/oidc/index.js'
export default defineComponent({ export default defineComponent({
name: 'LayoutHeader', name: 'LayoutHeader',
setup() { setup() {
@ -57,14 +58,7 @@ export default defineComponent({
async function handleSelect(key) { async function handleSelect(key) {
switch (key) { switch (key) {
case 'out': case 'out':
await logOut() await signoutRedirect()
}
}
async function logOut() {
const res = await userStore.userLogout()
if (res.code === 0) {
router.replace('/login')
} }
} }

View File

@ -32,15 +32,9 @@ import SideBar from './components/Sidebar/index.vue'
import Tags from './components/Tags/index.vue' import Tags from './components/Tags/index.vue'
import { useSettingStore } from '@/store/modules/setting.js' import { useSettingStore } from '@/store/modules/setting.js'
import { computed } from 'vue' import { computed } from 'vue'
import { useUserStore } from '@/store/modules/user.js'
const settingStore = useSettingStore() const settingStore = useSettingStore()
const menuMode = computed(() => settingStore.getMenuMode) const menuMode = computed(() => settingStore.getMenuMode)
const tagsMenuSetting = computed(() => settingStore.getTagsMenuSetting) const tagsMenuSetting = computed(() => settingStore.getTagsMenuSetting)
const useUser = useUserStore()
function getUserNow() {
useUser.getUserInfo()
}
getUserNow()
</script> </script>

View File

@ -1,44 +1,50 @@
/*
* @Author: whyafterme
* @Date: 2022-11-03 11:31:21
* @LastEditTime: 2022-11-21 09:02:20
* @LastEditors: whyafterme
* @Description:
* @FilePath: \new\src\router\guard\permission-guard.js
*/
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { usePermissionStore } from '@/store/modules/permission' import { usePermissionStore } from '@/store/modules/permission'
import { NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '@/router/routes' import { NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '@/router/routes'
import { getToken } from '@/utils/token' import { getUserInfo, signinRedirect, signoutRedirect } from '@/utils/oidc/index.js'
const WHITE_LIST = ['/login', '/redirect']
export function createPermissionGuard(router) { export function createPermissionGuard(router) {
const userStore = useUserStore() const userStore = useUserStore()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
router.beforeEach(async(to, from, next) => { router.beforeEach(async(to, from, next) => {
// const token = getToken() const oidcUser = await getUserInfo()
const token = true if (oidcUser) {
if (token) { const hasRoutes = !!permissionStore.permissionRoutes.length
if (to.path === '/login') { if (hasRoutes) {
next({ path: '/' }) next()
} else { } else {
const hasRoutes = !!permissionStore.permissionRoutes.length try {
if (hasRoutes) { const { clientRoleList } = oidcUser.profile
next() const { VITE_CLIENT_ID } = import.meta.env
} else { const { roleId } = clientRoleList.find((item) => {
try { return item.clientId === VITE_CLIENT_ID
// await userStore.getUserInfo() })
const routes = await permissionStore.generateRoutesMock() // await userStore.getUserInfo()
routes.forEach((item) => { // const routes = await permissionStore.generateRoutes(roleId)
router.addRoute(item) const routes = await permissionStore.generateRoutesMock()
}) routes.forEach((item) => {
router.addRoute(NOT_FOUND_ROUTE) router.addRoute(item)
router.addRoute(REDIRECT_ROUTE) })
next({ ...to, replace: true }) router.addRoute(NOT_FOUND_ROUTE)
} catch (error) { router.addRoute(REDIRECT_ROUTE)
// removeToken() next({ ...to, replace: true })
// $message.error(error) } catch (error) {
next({ path: '/login', query: { ...to.query, redirect: to.path }}) signinRedirect()
}
} }
} }
} else { } else {
if (WHITE_LIST.includes(to.path)) { if (to.path === '/login') {
next() next()
} else { } else {
next({ path: '/login', query: { ...to.query, redirect: to.path }}) signinRedirect()
} }
} }
}) })

View File

@ -17,8 +17,8 @@ const setting = {
}, },
/* tags */ /* tags */
tagsMenuSetting: { tagsMenuSetting: {
show: true, show: false,
fixed: true, fixed: false,
background: '#f5f7f9' background: '#f5f7f9'
} }
} }

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { asyncRoutes, basicRoutes } from '@/router/routes' import { asyncRoutes, basicRoutes } from '@/router/routes'
import { getMenu } from '@/api/auth/index' import { fetchPermission } from '@/api/auth/index'
import Layout from '@/layout/index.vue' import Layout from '@/layout/index.vue'
import modules from '@/utils/module.js' import modules from '@/utils/module.js'
@ -44,55 +44,43 @@ function filterAsyncRoutes(routes = [], role) {
return ret return ret
} }
/** function dealRoutes(routes) {
* @description:
* @param {*} routes
* @return {*}
*/
function dataArrayToRoutes(routes) {
const res = [] const res = []
routes.forEach(item => { routes.forEach(item => {
const tmp = { ...item } const tmp = { ...item }
// // 如果有component配置 if (tmp.parentId === 0) {
// if (tmp.component) {
// // Layout引入
// if (tmp.component === 'Layout') {
// tmp.component = Layout
// } else {
// const sub_view = tmp.component.replace(/^\/*/g, '')
// const component = `../${sub_view}.vue`
// tmp.component = modules[component]
// }
// if (tmp.children) {
// tmp.children = dataArrayToRoutes(tmp.children)
// }
// }
// 如果pid为0
if (tmp.pid === 0) {
// Layout引入
tmp.component = Layout tmp.component = Layout
if (tmp.children) { if (tmp.children) {
tmp.children = dataArrayToRoutes(tmp.children) tmp.redirect = tmp.path === '/' ? `/${tmp.children[0].path}` : `${tmp.path}/${tmp.children[0].path}`
tmp.children = dealRoutes(tmp.children)
} }
} else { } else {
const sub_view = tmp.component.replace(/^\/*/g, '') const sub_view = tmp.component.replace(/^\/*/g, '')
const component = `../${sub_view}.vue` const component = `../${sub_view}.vue`
tmp.component = modules[component] tmp.component = modules[component]
} }
tmp.name = tmp.title tmp.title = tmp.name
tmp.meta = { tmp.meta = {
...tmp.meta, ...tmp?.meta,
title: tmp.meta.title || tmp.title title: tmp.name
} }
res.push(tmp) res.push(tmp)
}) })
return res return res
} }
function dealPermissions(permissionsList) {
const res = permissionsList.map((tmp) => {
return tmp.code
})
return res
}
export const usePermissionStore = defineStore('permission', { export const usePermissionStore = defineStore('permission', {
state() { state() {
return { return {
accessRoutes: [] accessRoutes: [],
accessPermissions: []
} }
}, },
getters: { getters: {
@ -101,23 +89,31 @@ export const usePermissionStore = defineStore('permission', {
}, },
permissionRoutes() { permissionRoutes() {
return this.accessRoutes return this.accessRoutes
},
validatePermission() {
return this.accessPermissions
} }
}, },
actions: { actions: {
generateRoutesMock(role = []) { generateRoutesMock(role = []) {
const accessRoutes = filterAsyncRoutes(asyncRoutes, role) const accessRoutes = filterAsyncRoutes(asyncRoutes, role)
this.accessRoutes = accessRoutes this.accessRoutes = accessRoutes
console.log(accessRoutes)
return accessRoutes return accessRoutes
}, },
async generateRoutes() { async generateRoutes(roleId) {
try { try {
const res = await getMenu() const res = await fetchPermission(roleId)
// const res = await fetchPermission(3)
if (res.code === 0) { if (res.code === 0) {
const result = dataArrayToRoutes(res.data) const { opMenusList, permissionsList } = res.data
console.log(result) const menus = dealRoutes(opMenusList)
this.accessRoutes = result const permissions = dealPermissions(permissionsList)
return Promise.resolve(result) this.accessRoutes = menus
this.accessPermissions = permissions
return Promise.resolve(menus)
// const accessRoutes = filterAsyncRoutes(asyncRoutes)
// this.accessRoutes = accessRoutes
// return Promise.resolve(accessRoutes)
} else { } else {
return Promise.reject(res.message) return Promise.reject(res.message)
} }

View File

@ -1,71 +1,56 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { userLogin, loginOut } from '@/api/login'
import { getUser } from '@/api/login/index.js' import { getUser } from '@/api/login/index.js'
import { setToken, removeToken } from '@/utils/token'
import { useTagsMenuStore } from './tagsMenu.js'
import { usePermissionStore } from './permission.js'
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
persist: true, persist: {
enabled: true
},
state() { state() {
return { return {
userInfo: {} userInfo: {
hasLogin: false
}
} }
}, },
getters: { getters: {
userInfoMsg() { // userId() {
return this.userInfo // return this.userInfo?.id
// },
userName() {
return this.userInfo?.userName
},
realname() {
return this.userInfo?.realname
},
avatar() {
return this.userInfo?.avatar
},
role() {
return this.userInfo?.role || []
},
authority() {
return this.userInfo.authority
},
hasLogin() {
return this.userInfo.hasLogin
} }
}, },
actions: { actions: {
/* 登录 */ async getUserInfos() {
async getLoginToken(form) {
try { try {
const res = await userLogin(form) const res = await getUser()
if (res.code === 0) { if (res.code === 0) {
/* 设置token */ this.setUserInfo({ hasLogin: true, ...res.data })
setToken(res.data.access_token) return Promise.resolve(res.data.type)
this.getUserInfo()
return Promise.resolve(res)
} else { } else {
return Promise.reject(res) this.setUserInfo()
} }
} catch (error) { } catch (error) {
return Promise.reject(error) console.error(error)
this.setUserInfo()
} }
}, },
/* 获取用户信息 */ setUserInfo(userInfo = { hasLogin: false }) {
async getUserInfo() {
const res = await getUser()
if (res.code === 0) {
this.setUserInfo(res.data)
}
},
async userLogout() {
try {
const res = await loginOut()
if (res.code === 0) {
removeToken()
this.reset()
return Promise.resolve(res)
} else {
return Promise.reject(res)
}
} catch (error) {
return Promise.reject(error)
}
},
reset() {
this.$reset()
const tagsMenuStore = useTagsMenuStore()
const permissionStore = usePermissionStore()
tagsMenuStore.$reset()
permissionStore.$reset()
},
setUserInfo(userInfo = {}) {
this.userInfo = { ...this.userInfo, ...userInfo } this.userInfo = { ...this.userInfo, ...userInfo }
} }
} }

View File

@ -28,3 +28,20 @@ export const MENU_STATUS = [
{ label: '在用', value: 1 }, { label: '在用', value: 1 },
{ label: '停用', value: 2 } { label: '停用', value: 2 }
] ]
export const ADVERTISING_TYPE = [
{ label: '图片', value: 1, color: { color: '#f6ffed', textColor: '#389e0d', borderColor: '#b7eb8f' }},
{ label: '文字', value: 2, color: { color: '#e6f7ff', textColor: '#096dd9', borderColor: '#91d5ff' }},
{ label: '视频', value: 2, color: { color: '#fff1f0', textColor: '#cf1322', borderColor: '#ffa39e' }},
{ label: '推荐', value: 2, color: { color: '#fff7e6', textColor: '#d46b08', borderColor: '#ffd591' }}
]
export const ADVERTISING_PLATFORM = [
{ label: '微信小程序', value: 1, color: { color: '#e6f7ff', textColor: '#096dd9', borderColor: '#91d5ff' }},
{ label: '其他', value: 2, color: { color: '#f6ffed', textColor: '#389e0d', borderColor: '#b7eb8f' }}
]
export const ADVERTISING_STATUS = [
{ label: '正常', value: 1, color: { color: '#e6f7ff', textColor: '#096dd9', borderColor: '#91d5ff' }},
{ label: '停用', value: 2, color: { color: '#fff1f0', textColor: '#cf1322', borderColor: '#ffa39e' }}
]

View File

@ -1,6 +1,5 @@
import { router } from '@/router'
import { getToken, removeToken } from '@/utils/token'
import { isWithoutToken } from './help' import { isWithoutToken } from './help'
import { getUserInfo, signoutRedirect } from '@/utils/oidc/index.js'
export function setupInterceptor(service) { export function setupInterceptor(service) {
service.interceptors.request.use( service.interceptors.request.use(
@ -12,22 +11,20 @@ export function setupInterceptor(service) {
// 处理不需要token的请求 // 处理不需要token的请求
if (isWithoutToken(config)) { if (isWithoutToken(config)) {
return config return config
} else {
const userInfo = await getUserInfo()
if (userInfo) {
const { token_type, access_token } = userInfo
// config.headers.Authorization = `${token_type} ${access_token}`
config.headers.Authorization = '70aa58b4-dda7-446d-8cbf-8e6d6ab89a02'
const { VITE_CLIENT_ID } = import.meta.env
config.headers['Client-Id'] = VITE_CLIENT_ID
return config
} else {
signoutRedirect()
return Promise.reject({ response: { status: 401, message: '未登录' }})
}
} }
const token = getToken()
if (token) {
config.headers.Authorization = token
return config
}
/**
* * 未登录或者token过期的情况下
* * 跳转登录页重新登录携带当前路由及参数登录成功会回到原来的页面
*/
const { currentRoute } = router
router.replace({
path: '/login',
query: { ...currentRoute.query, redirect: currentRoute.path }
})
return Promise.reject({ code: '-1', message: '未登录' })
}, },
(error) => Promise.reject(error) (error) => Promise.reject(error)
) )
@ -36,7 +33,6 @@ export function setupInterceptor(service) {
(response) => { (response) => {
const { method } = response?.config const { method } = response?.config
const { code } = response?.data const { code } = response?.data
const { currentRoute } = router
switch (code) { switch (code) {
case 0: case 0:
if (method !== 'get') { if (method !== 'get') {
@ -48,11 +44,7 @@ export function setupInterceptor(service) {
break break
case 401: case 401:
// 未登录可能是token过期或者无效了 // 未登录可能是token过期或者无效了
removeToken() signoutRedirect()
router.replace({
path: '/login',
query: { ...currentRoute.query, redirect: currentRoute.path }
})
break break
default: default:
break break
@ -60,7 +52,15 @@ export function setupInterceptor(service) {
return response?.data return response?.data
}, },
(error) => { (error) => {
return Promise.reject(error) const { status } = error.response
if (status === 401) {
signoutRedirect()
} else if (status === 403) {
$message.error('暂无权限访问,请联系管理员')
return Promise.reject(error)
} else {
return Promise.reject(error)
}
} }
) )
} }

91
src/utils/oidc/index.js Normal file
View File

@ -0,0 +1,91 @@
import { UserManager } from 'oidc-client'
import { createLocalStorage } from '../cache'
let oidcManager = null
export const initServe = () => {
if (oidcManager) return oidcManager
const { VITE_AUTHORITY, VITE_CLIENT_ID, VITE_CLIENT_SECRET, VITE_REDIRECT_URI } = import.meta.env
oidcManager = new UserManager({
/* 认证服务器 */
authority: VITE_AUTHORITY,
/* 客户端id */
client_id: VITE_CLIENT_ID,
client_secret: VITE_CLIENT_SECRET,
/* 回调客户端页面 */
redirect_uri: VITE_REDIRECT_URI,
post_logout_redirect_uri: VITE_REDIRECT_URI,
response_type: 'code',
/* 授权范围 */
scope: 'openid profile',
automaticSilentRenew: true,
revokeAccessTokenOnSignout: true,
metadata: {
issuer: `${VITE_AUTHORITY}`,
authorization_endpoint: `${VITE_AUTHORITY}/oauth2/authorize`,
token_endpoint: `${VITE_AUTHORITY}/oauth2/token`,
userinfo_endpoint: `${VITE_AUTHORITY}/userinfo`,
end_session_endpoint: `${VITE_AUTHORITY}/toLogout`,
revocation_endpoint: `${VITE_AUTHORITY}/oauth2/revoke`
}
})
return oidcManager
}
export const getUserInfo = async() => {
oidcManager = initServe()
return await oidcManager.getUser()
}
/* 登录 */
export const signinRedirect = async() => {
oidcManager = initServe()
oidcManager.signinRedirect({})
.then(res => {
setPath(window.location.pathname)
console.log('signed in', res)
}).catch(err => {
console.err(err)
})
}
/* 登录回调 */
export const signinRedirectCallback = async() => {
const userInfo = await getUserInfo()
if (!userInfo) {
oidcManager = initServe()
return await oidcManager.signinRedirectCallback()
}
}
/* 退出 */
export const signoutRedirect = () => {
oidcManager = initServe()
oidcManager.signoutRedirect({})
.then(function(res) {
setPath(window.location.pathname)
console.log('signed out', res)
}).catch(function(err) {
console.log(err)
})
}
/* 退出回调 */
export const signoutRedirectCallback = async() => {
const userInfo = await getUserInfo()
if (!userInfo) {
oidcManager = initServe()
return await oidcManager.signoutRedirectCallback()
}
}
const PATH_CODE = 'redirect_path'
const DURATION = 24 * 60 * 60
export const isPath = createLocalStorage()
/* 获取Path */
export function getPath() {
return isPath.get(PATH_CODE)
}
/* 设置Path */
export function setPath(Path, duration = DURATION) {
isPath.set(PATH_CODE, Path, duration)
}
/* 移出Path */
export function removePath() {
isPath.remove(PATH_CODE)
}

View File

@ -1,17 +1,78 @@
<template> <template>
<div> <div class="login">
登录页 <n-spin>
<template #description>
正在进入系统中...
</template>
</n-spin>
</div> </div>
</template> </template>
<script> <script>
export default { import { useRoute, useRouter } from 'vue-router'
name: '', import { useUserStore } from '@/store/modules/user'
setup() { import { fetchPermission } from '@/api/auth/index'
import { signinRedirectCallback, signoutRedirectCallback, signoutRedirect, getPath, removePath } from '@/utils/oidc/index.js'
export default {
name: 'LoginPage',
setup() {
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
if (route.query?.code && route.query?.state) {
/* 登录后的回调 */
signinRedirectCallback().then(res => {
// router.push({ path: getPath() })
// removePath()
// userStore.getUserInfos()
/* 判断返回的授权信息是否能访问该平台 */
const { authority, clientRoleList } = res.profile
const { VITE_PLATFORM, VITE_CLIENT_ID } = import.meta.env
const { roleId } = clientRoleList.find((item) => {
return item.clientId === VITE_CLIENT_ID
})
if (authority && (authority.includes(VITE_PLATFORM) || authority.includes('admin'))) {
fetchPermission(roleId)
.then(res => {
const { opMenusList } = res.data
const path = opMenusList[0].path
router.push(path)
})
// router.push('/')
} else {
$message.error('暂无权限访问,请联系管理员')
setTimeout(() => {
signoutRedirect()
}, 2000)
}
}).catch(err => {
console.log(err)
/* 失败则退出登录 */
signoutRedirect()
})
// } else if (getPath()) {
// signoutRedirectCallback().then(res => {
// router.push({ path: getPath() })
// userStore.setUserInfo()
// removePath()
// })
}
} }
} }
</script> </script>
<style scoped lang='scss'> <style scoped lang='scss'>
.login{
width: 100vw;
height: 100vh;
position: relative;
.n-spin-body{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
</style> </style>

View File

@ -0,0 +1,17 @@
<template>
<div>
河道管理
</div>
</template>
<script>
export default {
name: 'RiverManage',
setup() {
}
}
</script>
<style scoped lang='scss'>
</style>

View File

@ -0,0 +1,189 @@
<template>
<Modal
:options="getModalOptions"
:on-positive-click="handleConfirm"
:on-negative-click="handleClose"
:on-close="handleClose"
>
<template #Context>
<n-form
ref="formRef"
:model="advertisingForm"
:rules="advertisingRules"
:label-width="100"
label-placement="left"
require-mark-placement="left"
:disabled="disabled"
>
<div class="flex__body">
<template v-for="(item,index) in getFormOptions" :key="index">
<n-form-item :label="item.label" :path="item.key" :class="{ 'flex__item': !['oss','editor'].includes(item.type) }">
<UploadOss v-if="item.type === 'oss'" :ref="el=>{ossRefs[item.refIndex] = el}" :default-list="advertisingForm[item.file]" @upload-status="handleUploadStatus" />
<n-input v-if="item.type === 'input'" v-model:value="advertisingForm[item.key]" v-bind="item.props" />
<n-select v-if="item.type === 'select'" v-model:value="advertisingForm[item.key]" v-bind="item.props" />
<n-radio-group v-if="item.type === 'radio'" v-model:value="advertisingForm[item.key]" :name="item.key">
<n-space>
<n-radio v-for="(cItem,cIndex) in item.options" :key="`${item.key}_${cIndex}`" :value="cItem.value"> {{ cItem.label }}</n-radio>
</n-space>
</n-radio-group>
<n-date-picker v-if="item.type === 'date'" v-model:formatted-value="advertisingForm[item.key]" v-bind="item.props" />
<n-input-number v-if="item.type === 'number'" v-model:value="advertisingForm[item.key]" v-bind="item.props" />
<TinymceEditor v-if="item.type === 'editor'" v-model:modelValue="advertisingForm[item.key]" v-bind="item.props" />
</n-form-item>
</template>
</div>
</n-form>
</template>
</Modal>
</template>
<script>
import { form } from '../tools/form.js'
import Modal from '@/components/Modal/index.vue'
import UploadOss from '@/components/UploadOss/index.vue'
import TinymceEditor from '@/components/TinymceEditor/index.vue'
import { advertisingCreate, advertisingUpdate } from '@/api/setting/advertising.js'
import { defineComponent, ref, reactive, computed, toRefs } from 'vue'
export default defineComponent({
name: 'UserModal',
components: { Modal, UploadOss, TinymceEditor },
props: {
visible: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'create'
},
data: {
type: Object,
default: () => {}
}
},
emits: {
'update:visible': null,
'reload': null
},
setup(props, { emit }) {
const MODAL_TYPE = {
'create': '新建广告',
'preview': '广告详情',
'update': '编辑广告'
}
const { advertisingForm, advertisingRules } = form
const formRef = ref()
const ossRefs = ref([])
const data = reactive({
advertisingForm: {
...advertisingForm,
imageStatus: '',
...props.data
},
advertisingRules: {
...advertisingRules
},
disabled: props.type === 'preview'
})
const getModalOptions = computed(() => {
return {
title: MODAL_TYPE[props.type],
width: 800,
show: props.visible,
trapFocus: false,
negativeText: '取消',
positiveText: '确认'
}
})
const getFormOptions = computed(() => {
return {
...form.formItem
}
})
function handleConfirm() {
formRef.value.validate((errors) => {
if (!errors) {
const uploads = ossRefs.value.map((item, index) => {
return item.startUpload()
})
Promise.all(uploads)
.then(response => {
const isError = response.map((item) => {
return item.includes('error')
})
if (!isError.includes(true)) {
const imageStr = response.join()
const params = { ...data.advertisingForm, cover: imageStr }
if (params.id) {
advertisingUpdate(params).then(res => {
if (res.code === 0) {
emit('reload')
handleClose()
}
}).catch(e => {
console.log(e)
})
} else {
advertisingCreate(params).then(res => {
if (res.code === 0) {
emit('reload')
handleClose()
}
})
}
} else {
$message.error('文件上传失败,请稍后重试')
}
})
}
})
}
/* 关闭弹窗 */
const handleClose = () => {
emit('update:visible', false)
}
function handleUploadStatus(status) {
data.advertisingForm.imageStatus = status
}
return {
...toRefs(data),
formRef,
ossRefs,
getModalOptions,
getFormOptions,
handleConfirm,
handleClose,
handleUploadStatus
}
}
})
</script>
<style scoped lang='scss'>
.n-input-number{
width: 100%;
}
.flex__body{
display: flex;
width: 100%;
flex-wrap: wrap;
.n-form-item{
width: 100%;
}
.flex__item{
width: 50%;
}
}
.n-date-picker{
width: 100%;
}
</style>

View File

@ -1,17 +1,72 @@
<template> <template>
<div> <div>
广告管理 <n-card>
<headSearch :info="search" @search="handleSearch" @reset="handleSearch" />
<data-table
ref="tableRef"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
:scroll-x="2200"
size="large"
>
<template #tableTitle>
<n-button type="primary" @click="handleModal"> 新建 </n-button>
</template>
</data-table>
</n-card>
</div> </div>
<!-- 新增编辑弹窗 -->
<AdvertisingModal v-if="modalShow" v-model:visible="modalShow" :data="rowData" :type="modalType" @reload="handleSearch" />
</template> </template>
<script> <script>
import search from './tools/search.js'
import table from './tools/table.js'
import headSearch from '@/components/Search/index.vue'
import dataTable from '@/components/DataTable/index.vue'
import AdvertisingModal from './components/AdvertisingModal.vue'
import { fetchAdList } from '@/api/setting/advertising.js'
import { unref, toRefs, reactive, onUnmounted } from 'vue'
export default { export default {
name: 'AdvertisingPage', name: 'AdvertisingPage',
components: { dataTable, AdvertisingModal, headSearch },
setup() { setup() {
const data = reactive({
...toRefs(table),
search
})
const loadDataTable = async(res) => {
const _params = {
...unref(data.searchParams),
...res
}
return await fetchAdList(_params)
}
//
function handleModal() {
data.rowData = null
data.modalType = 'create'
data.modalShow = true
}
onUnmounted(() => {
data.searchParams = null
})
return {
...toRefs(data),
loadDataTable,
handleModal
}
} }
} }
</script> </script>
<style scoped lang='scss'> <style scoped lang='scss'>
.n-button + .n-button {
margin-left: 10px;
}
</style> </style>

View File

@ -0,0 +1,57 @@
import { ref, reactive } from 'vue'
import { ADVERTISING_TYPE, ADVERTISING_PLATFORM, ADVERTISING_STATUS } from '@/utils/dictionary.js'
export const form = reactive({
advertisingForm: {
cover: null,
title: null,
description: null,
type: 1,
platform: 1,
url: null,
status: 1,
width: null,
height: null,
startTime: null,
endTime: null,
sort: null,
content: null
},
advertisingRules: {
title: [{ required: true, message: '请输入广告标题', trigger: 'blur' }],
sort: [{ required: true, type: 'number', message: '请输入排序号', trigger: 'blur' }]
},
formItem: [
{ type: 'oss', refIndex: 0, key: 'imageStatus', file: 'cover', label: '广告图片', props: { maxlength: '20', placeholder: '请输入部门编号', clearable: true }},
{ type: 'input', key: 'title', label: '广告标题', props: { maxlength: '20', placeholder: '请输入广告标题', clearable: true }},
{ type: 'input', key: 'description', label: '广告描述', props: { maxlength: '20', placeholder: '请输入广告描述', clearable: true }},
{ type: 'select', key: 'type', label: '广告类型', props: { options: ADVERTISING_TYPE, placeholder: '请选择广告类型' }},
{ type: 'select', key: 'platform', label: '投放平台', props: { options: ADVERTISING_PLATFORM, placeholder: '请选择投放平台' }},
{ type: 'input', key: 'url', label: '广告URL', props: { maxlength: '20', placeholder: '请输入广告URL', clearable: true }},
{ type: 'radio', key: 'status', label: '广告状态', options: ADVERTISING_STATUS, mode: ['config'] },
{ type: 'number', key: 'width', label: '广告宽度', props: { min: 0, placeholder: '请输入广告宽度', showButton: false, clearable: true }},
{ type: 'number', key: 'height', label: '广告高度', props: { min: 0, placeholder: '请输入广告高度', showButton: false, clearable: true }},
{ type: 'date', key: 'startTime', label: '开始时间', props: {
type: 'datetime',
valueFormat: 'yyyy-MM-dd HH:mm:ss', format: 'yyyy-MM-dd HH:mm:ss',
actions: ['clear', 'confirm'],
timePickerProps: { actions: ['confirm'] }
}},
{ type: 'date', key: 'endTime', label: '结束时间', props: {
type: 'datetime',
valueFormat: 'yyyy-MM-dd HH:mm:ss', format: 'yyyy-MM-dd HH:mm:ss',
actions: ['clear', 'confirm'],
timePickerProps: { actions: ['confirm'] }
}},
{ type: 'number', key: 'sort', label: '排序号', props: { min: 0, placeholder: '请输入排序号', showButton: false, clearable: true }},
{ type: 'editor', key: 'content', label: '广告内容', props: { height: 300 }}
]
})

View File

@ -0,0 +1,14 @@
import { reactive } from 'vue'
const data = reactive([
{
label: '广告名称',
key: 'title',
props: {
placeholder: '请输入广告名称'
}
}
])
export default data

View File

@ -0,0 +1,237 @@
import { h, ref, reactive } from 'vue'
import TableImage from '@/components/DataTable/tools/Image.vue'
import TableTags from '@/components/DataTable/tools/Tags.vue'
import TableAction from '@/components/DataTable/tools/Action.vue'
import { ADVERTISING_TYPE, ADVERTISING_PLATFORM, ADVERTISING_STATUS } from '@/utils/dictionary.js'
import { advertisingDelete } from '@/api/setting/advertising.js'
/* 注册table */
const tableRef = ref()
const searchParams = ref()
function handleSearch(params) {
searchParams.value = { ...params }
tableRef.value.reFetch({ searchParams })
}
/**
* @description: 获取数据及操作
* @param {*} row 单行数据
* @param {*} type 操作类型 create:创建preview:预览edit:编辑
* @return {*}
*/
function getRowData(row, type) {
data.rowData = type === 'create' ? { pid: row.id } : row
data.modalType = type
data.modalShow = true
}
// 删除方法
function deleteData(id) {
advertisingDelete(id)
.then((res) => {
if (res.code === 0) {
handleSearch()
}
})
.catch((e) => {
console.log(e)
})
}
const data = reactive({
tableRef,
searchParams,
rowData: {},
modalType: 'create',
modalShow: false,
handleSearch,
columns: [
{
type: 'selection'
},
{
title: '编号',
key: 'key',
render: (_, index) => {
return `${index + 1}`
},
align: 'center',
width: 50
},
{
title: '广告名称',
key: 'title',
align: 'center',
width: 200
},
{
title: '广告封面',
key: 'cover',
render(row) {
return h(TableImage, {
images: {
width: 36,
height: 36,
src: row.cover
}
})
},
align: 'center',
width: 100
},
{
title: '广告类型',
key: 'type',
align: 'center',
width: 100,
render(row) {
return h(TableTags, {
data: row.type,
filters: ADVERTISING_TYPE,
tags: {
bordered: true
}
})
}
},
{
title: '投放平台',
key: 'platform',
align: 'center',
width: 100,
render(row) {
return h(TableTags, {
data: row.platform,
filters: ADVERTISING_PLATFORM,
tags: {
bordered: true
}
})
}
},
{
title: '广告描述',
key: 'description',
align: 'center',
ellipsis: {
tooltip: true
},
width: 250
},
{
title: '广告地址',
key: 'url',
align: 'center',
ellipsis: {
tooltip: true
},
width: 200
},
{
title: '广告尺寸',
key: 'size',
align: 'center',
render(row) {
return h(TableTags, {
data: `${row.width} x ${row.height}`
})
},
width: 100
},
{
title: '开始时间',
key: 'startTime',
align: 'center',
width: 200
},
{
title: '结束时间',
key: 'endTime',
align: 'center',
width: 200
},
{
title: '浏览量',
key: 'viewNum',
align: 'center',
width: 100
},
{
title: '状态',
key: 'status',
align: 'center',
width: 50,
render(row) {
return h(TableTags, {
data: row.status,
filters: ADVERTISING_STATUS,
tags: {
bordered: true
}
})
}
},
{
title: '排序',
key: 'sort',
align: 'center',
width: 50
},
{
title: '创建时间',
key: 'createTime',
align: 'center',
width: 200
},
{
title: '操作',
align: 'center',
width: 150,
fixed: 'right',
render(row) {
return h(TableAction, {
actions: [
{
label: '添加',
type: 'button',
props: {
type: 'primary',
text: true,
onClick: getRowData.bind(null, row, 'create')
},
auth: 'basic_list'
},
{
label: '修改',
type: 'button',
props: {
type: 'primary',
text: true,
onClick: getRowData.bind(null, row, 'update')
},
auth: 'basic_list'
},
{
label: '删除',
type: 'popconfirm',
auth: 'basic_list',
tip: '确定删除这条数据吗?',
props: {
onPositiveClick: deleteData.bind(null, [row.id])
},
ButtonProps: {
text: true,
type: 'primary'
}
}
],
align: 'center'
})
}
}
]
})
export default data

View File

@ -1,14 +1,32 @@
<template> <template>
<div> <div>
统计分析 <n-button v-if="validatePermission.includes('sys:permission:testOne')" @click="handleOne">这是按钮one</n-button>
<n-button v-if="validatePermission.includes('sys:permission:testTwo')" @click="handleTwo">这是按钮two</n-button>
</div> </div>
</template> </template>
<script> <script>
import { usePermissionStore } from '@/store/modules/permission.js'
import { permissionOne, permissionTwo } from '@/api/auth/index.js'
import { computed } from 'vue'
import { signinRedirectCallback, signoutRedirectCallback, signoutRedirect, getPath, removePath } from '@/utils/oidc/index.js'
export default { export default {
name: 'StatisticalAnalysis', name: 'StatisticalAnalysis',
setup() { setup() {
const { validatePermission } = usePermissionStore()
const handleOne = async() => {
const res = await permissionOne()
alert(res)
}
const handleTwo = async() => {
const res = await permissionTwo()
}
return {
handleOne,
handleTwo,
validatePermission
}
} }
} }