对接统一登录认证
This commit is contained in:
@@ -4,4 +4,5 @@ ENV = 'development'
|
||||
# base api
|
||||
VUE_APP_BASE_API = '/dev-api'
|
||||
|
||||
VUE_APP_PROXY_API = 'https://www.yyds8848.com/ca/'
|
||||
# VUE_APP_PROXY_API = 'https://www.yyds8848.com/ca/'
|
||||
VUE_APP_PROXY_API = 'http://127.0.0.1:60000/'
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
"dependencies": {
|
||||
"axios": "1.8.2",
|
||||
"core-js": "^3.42.0",
|
||||
"echarts": "^5.6.0",
|
||||
"element-ui": "2.13.2",
|
||||
"js-cookie": "2.2.0",
|
||||
"js-sha256": "^0.11.1",
|
||||
"normalize.css": "7.0.0",
|
||||
"nprogress": "0.2.0",
|
||||
"path-to-regexp": "2.4.0",
|
||||
|
||||
@@ -22,3 +22,11 @@ export function logout() {
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function callback(code) {
|
||||
return request({
|
||||
url: '/user/oauth/login/callback',
|
||||
method: 'get',
|
||||
params: { code }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,12 +8,11 @@ import getPageTitle from '@/utils/get-page-title'
|
||||
|
||||
NProgress.configure({ showSpinner: false }) // NProgress Configuration
|
||||
|
||||
const whiteList = ['/login'] // no redirect whitelist
|
||||
const whiteList = ['/login', '/social', '/callback'] // no redirect whitelist
|
||||
|
||||
router.beforeEach(async(to, from, next) => {
|
||||
// start progress bar
|
||||
NProgress.start()
|
||||
|
||||
// set page title
|
||||
document.title = getPageTitle(to.meta.title)
|
||||
|
||||
@@ -46,7 +45,6 @@ router.beforeEach(async(to, from, next) => {
|
||||
}
|
||||
} else {
|
||||
/* has no token*/
|
||||
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
// in the free login whitelist, go directly
|
||||
next()
|
||||
|
||||
@@ -36,7 +36,15 @@ export const constantRoutes = [
|
||||
component: () => import('@/views/login/index'),
|
||||
hidden: true
|
||||
},
|
||||
|
||||
{
|
||||
path: '/social',
|
||||
component: () => import('@/views/login/social/index.vue'),
|
||||
hidden: true
|
||||
}, {
|
||||
path: '/callback',
|
||||
component: () => import('@/views/login/social/callback.vue'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
component: () => import('@/views/404'),
|
||||
@@ -80,8 +88,8 @@ export const constantRoutes = [
|
||||
]
|
||||
|
||||
const createRouter = () => new Router({
|
||||
// mode: 'history', // require service support
|
||||
mode: 'hash',
|
||||
mode: 'history', // require service support
|
||||
// mode: 'hash',
|
||||
scrollBehavior: () => ({ y: 0 }),
|
||||
routes: constantRoutes
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
|
||||
title: 'CA 证书签发平台',
|
||||
title: 'CA 证书签发管理系统',
|
||||
|
||||
/**
|
||||
* @type {boolean} true | false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { login, logout, getInfo } from '@/api/user'
|
||||
import { login, logout, getInfo, callback } from '@/api/user'
|
||||
import { getToken, setToken, removeToken } from '@/utils/auth'
|
||||
import { resetRouter } from '@/router'
|
||||
|
||||
@@ -33,9 +33,22 @@ const actions = {
|
||||
const { username, password } = userInfo
|
||||
return new Promise((resolve, reject) => {
|
||||
login({ username: username.trim(), password: password }).then(response => {
|
||||
const { data } = response
|
||||
commit('SET_TOKEN', data.token)
|
||||
setToken(data.token)
|
||||
const { access_token } = response.data
|
||||
commit('SET_TOKEN', access_token)
|
||||
setToken(access_token)
|
||||
resolve()
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
callback({ commit }, code) {
|
||||
return new Promise((resolve, reject) => {
|
||||
callback(code).then(response => {
|
||||
const { access_token } = response.data
|
||||
commit('SET_TOKEN', access_token)
|
||||
setToken(access_token)
|
||||
resolve()
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
|
||||
42
ca-admin-ui/src/utils/oauth-utils.js
Normal file
42
ca-admin-ui/src/utils/oauth-utils.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// 使用 js-sha256 库
|
||||
import sha256 from 'js-sha256';
|
||||
|
||||
// 生成 code_verifier
|
||||
export function generateCodeVerifier() {
|
||||
const array = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(array);
|
||||
return base64UrlEncode(array);
|
||||
}
|
||||
|
||||
// Base64 URL 编码
|
||||
function base64UrlEncode(uint8Array) {
|
||||
return btoa(String.fromCharCode.apply(null, uint8Array))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
}
|
||||
|
||||
// 使用 js-sha256 生成 code_challenge
|
||||
export function generateCodeChallenge(codeVerifier) {
|
||||
const hash = sha256.arrayBuffer(codeVerifier); // 返回 ArrayBuffer
|
||||
return base64UrlEncode(new Uint8Array(hash));
|
||||
}
|
||||
|
||||
// 构建授权 URL
|
||||
export function buildAuthorizeUrl(
|
||||
oauthServer,
|
||||
clientId,
|
||||
redirectUri,
|
||||
state,
|
||||
scope
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope,
|
||||
state,
|
||||
});
|
||||
|
||||
return `${oauthServer}/oauth2/authorize?${params.toString()}`;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<div slot="header" class="chart-header">
|
||||
<span>订单分布</span>
|
||||
<el-select v-model="chartType" size="small" @change="handleTypeChange">
|
||||
<el-option label="按平台" value="platform" />
|
||||
<el-option label="按地区" value="region" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div ref="chart" class="chart-content" />
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'OrderDistributionChart',
|
||||
data() {
|
||||
return {
|
||||
chartType: 'platform',
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.chart = echarts.init(this.$refs.chart)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
color: '#606266'
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '订单来源',
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '18',
|
||||
fontWeight: 'bold',
|
||||
formatter: '{b}\n{c} ({d}%)'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{ value: 335, name: '平台A', itemStyle: { color: '#409EFF' }},
|
||||
{ value: 310, name: '平台B', itemStyle: { color: '#67C23A' }},
|
||||
{ value: 274, name: '平台C', itemStyle: { color: '#E6A23C' }},
|
||||
{ value: 235, name: '平台D', itemStyle: { color: '#F56C6C' }},
|
||||
{ value: 400, name: '平台E', itemStyle: { color: '#9C27B0' }}
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
||||
this.chart.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
if (this.chart) {
|
||||
this.chart.resize()
|
||||
}
|
||||
},
|
||||
handleTypeChange() {
|
||||
// 实际项目中可以在这里触发数据更新
|
||||
// this.$emit('type-change', this.chartType)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
|
||||
.chart-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.chart-card {
|
||||
.chart-content {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
239
ca-admin-ui/src/views/dashboard/components/QuickActions.vue
Normal file
239
ca-admin-ui/src/views/dashboard/components/QuickActions.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<el-row :gutter="20" class="quick-actions-container">
|
||||
<!-- Left Column - Project Info -->
|
||||
<el-col :xs="24" :md="18" class="project-info-col">
|
||||
<el-row :gutter="20" class="projects-container">
|
||||
<el-col v-for="(project, idx) in projects.slice(0, 12)" :key="idx" :xs="24" :sm="6">
|
||||
<el-card
|
||||
shadow="hover"
|
||||
class="project-info-card"
|
||||
style="cursor: pointer;"
|
||||
@click.native="openProject(project.url)"
|
||||
>
|
||||
<div class="project-info">
|
||||
<div class="project-icon">
|
||||
<i :class="['el-icon-' + project.icon]" />
|
||||
</div>
|
||||
<div class="project-details">
|
||||
<h3 class="project-name">{{ project.name }}</h3>
|
||||
<p class="project-description">{{ project.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<!-- Right Column - Action Matrix -->
|
||||
<el-col :xs="24" :md="6" class="action-matrix-col">
|
||||
<el-row :gutter="16" class="action-matrix">
|
||||
<el-col
|
||||
v-for="(action, idx) in actions"
|
||||
:key="idx"
|
||||
:xs="8"
|
||||
:sm="8"
|
||||
class="action-cell"
|
||||
>
|
||||
<div class="matrix-action-item" @click="handleActionClick(action.path)">
|
||||
<div class="matrix-action-icon" :style="{ backgroundColor: action.color }">
|
||||
<i :class="['btn-icon', action.icon]" />
|
||||
</div>
|
||||
<span class="matrix-action-label">{{ action.label }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'QuickActions',
|
||||
props: {
|
||||
projects: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
actions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleActionClick(path) {
|
||||
this.$emit('action-click', path)
|
||||
},
|
||||
openProject(url) {
|
||||
window.open(url, '_blank') // 新标签页打开项目链接
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>.quick-actions-container {
|
||||
|
||||
.project-info-col {
|
||||
.projects-container {
|
||||
.project-info-card {
|
||||
height: 140px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px; // 略微减少底部间距
|
||||
|
||||
.project-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px; // 减少内边距
|
||||
|
||||
.project-icon {
|
||||
width: 40px; // 缩小图标容器
|
||||
height: 40px;
|
||||
border-radius: 10px; // 略微减少圆角半径
|
||||
background-color: #409EFF20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px; // 减少右边距
|
||||
flex-shrink: 0;
|
||||
|
||||
i {
|
||||
font-size: 20px; // 缩小图标字体大小
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.project-details {
|
||||
flex: 1;
|
||||
|
||||
.project-name {
|
||||
margin: 0 0 6px 0; // 调整间距
|
||||
color: #303133;
|
||||
font-size: 16px; // 略微减小字体大小
|
||||
}
|
||||
|
||||
.project-description {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 12px; // 减小字体大小
|
||||
line-height: 1.4; // 略微减少行高
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-matrix-col {
|
||||
.action-matrix {
|
||||
height: 100%;
|
||||
|
||||
.action-cell {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.matrix-action-item {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px 7px; // 减少内边距
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
border-color: #409EFF;
|
||||
|
||||
.matrix-action-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-action-icon {
|
||||
width: 25px; // 略微缩小图标容器
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 5px; // 减少底部间距
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.btn-icon {
|
||||
font-size: 18px; // 缩小图标字体大小
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-action-label {
|
||||
display: block;
|
||||
font-size: 11px; // 减小字体大小
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.quick-actions-container {
|
||||
.project-info-col {
|
||||
.projects-container {
|
||||
.project-info-card {
|
||||
padding: 8px; // 进一步减少内边距
|
||||
|
||||
.project-icon {
|
||||
width: 36px; // 移动端进一步缩小图标
|
||||
height: 36px;
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-details {
|
||||
.project-name {
|
||||
font-size: 14px; // 移动端字体更小
|
||||
}
|
||||
|
||||
.project-description {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-matrix-col {
|
||||
.action-matrix {
|
||||
.action-cell {
|
||||
.matrix-action-item {
|
||||
padding: 8px 4px; // 减少内边距
|
||||
|
||||
.matrix-action-icon {
|
||||
width: 32px; // 移动端进一步缩小图标
|
||||
height: 32px;
|
||||
|
||||
.btn-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-action-label {
|
||||
font-size: 10px; // 更小的字体大小
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
76
ca-admin-ui/src/views/dashboard/components/RecentOrders.vue
Normal file
76
ca-admin-ui/src/views/dashboard/components/RecentOrders.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<el-card class="order-card" shadow="hover">
|
||||
<div slot="header" class="order-header">
|
||||
<span>最新订单</span>
|
||||
<el-button type="text" @click="handleViewMore">查看更多</el-button>
|
||||
</div>
|
||||
<el-table :data="orders" style="width: 100%" class="order-table">
|
||||
<el-table-column prop="id" label="订单号" width="120" />
|
||||
<el-table-column prop="customer" label="客户" width="120" />
|
||||
<el-table-column prop="product" label="产品" />
|
||||
<el-table-column prop="amount" label="金额" width="120">
|
||||
<template #scope>
|
||||
<span style="color: #F56C6C; font-weight: bold;">¥{{ scope.row.amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #scope>
|
||||
<el-tag :type="getStatusTagType(scope.row.status)" size="small">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #scope>
|
||||
<el-button type="text" size="small" @click="handleViewDetail(scope.row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RecentOrders',
|
||||
props: {
|
||||
orders: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getStatusTagType(status) {
|
||||
const map = {
|
||||
'已完成': 'success',
|
||||
'处理中': 'primary',
|
||||
'已发货': 'warning',
|
||||
'待付款': 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
},
|
||||
handleViewDetail(order) {
|
||||
this.$emit('view-detail', order)
|
||||
},
|
||||
handleViewMore() {
|
||||
this.$emit('view-more')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-card {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
149
ca-admin-ui/src/views/dashboard/components/SalesTrendChart.vue
Normal file
149
ca-admin-ui/src/views/dashboard/components/SalesTrendChart.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<div slot="header" class="chart-header">
|
||||
<span>销售趋势图</span>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
size="small"
|
||||
placeholder="选择日期范围"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</div>
|
||||
<div ref="chart" class="chart-content" />
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'SalesTrendChart',
|
||||
data() {
|
||||
return {
|
||||
dateRange: [new Date(2023, 0, 1), new Date(2023, 6, 1)],
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.chart = echarts.init(this.$refs.chart)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}<br/>{a0}: {c0}元',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月'],
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#DCDFE6'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#606266'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#606266',
|
||||
formatter: '{value}元'
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: ['#EBEEF5'],
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: '销售额',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [12000, 15000, 13000, 18000, 20000, 25000, 22000],
|
||||
itemStyle: {
|
||||
color: '#409EFF'
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(64, 158, 255, 0.5)' },
|
||||
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
|
||||
])
|
||||
},
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
width: 3
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
this.chart.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
if (this.chart) {
|
||||
this.chart.resize()
|
||||
}
|
||||
},
|
||||
handleDateChange() {
|
||||
// 实际项目中可以在这里触发数据更新
|
||||
// this.$emit('date-change', this.dateRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
|
||||
.chart-header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.chart-card {
|
||||
.chart-content {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
247
ca-admin-ui/src/views/dashboard/components/StatCards.vue
Normal file
247
ca-admin-ui/src/views/dashboard/components/StatCards.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<el-row :gutter="20" class="stat-row">
|
||||
<el-col v-for="(item, index) in statsList" :key="index" :xs="24" :sm="12" :md="6">
|
||||
<el-card class="stat-card" shadow="hover">
|
||||
<transition name="fade">
|
||||
<div :key="item.value" class="stat-content">
|
||||
<div class="icon-wrapper" :style="{ backgroundColor: item.color + '20' }">
|
||||
<i :class="['icon', item.icon]" :style="{ color: item.color }" />
|
||||
</div>
|
||||
<div class="stat-text">
|
||||
<p class="label">{{ item.label }}</p>
|
||||
<h3 class="value">{{ formattedValues[index] }}</h3>
|
||||
<div v-if="item.trend" class="trend">
|
||||
<i
|
||||
:class="['el-icon-' + (item.trend > 0 ? 'top' : 'bottom')]"
|
||||
:style="{ color: item.trend > 0 ? '#67C23A' : '#F56C6C' }"
|
||||
/>
|
||||
<span :style="{ color: item.trend > 0 ? '#67C23A' : '#F56C6C' }">
|
||||
{{ Math.abs(item.trend) }}%
|
||||
</span>
|
||||
<span class="period">较上月</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StatCards',
|
||||
props: {
|
||||
statsList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
animatedValues: [],
|
||||
formattedValues: [],
|
||||
animationFrames: {} // Initialize animationFrames here
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
statsList: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
// Initialize arrays with proper length
|
||||
this.animatedValues = new Array(newVal.length).fill(0)
|
||||
this.formattedValues = new Array(newVal.length).fill('')
|
||||
|
||||
newVal.forEach((item, index) => {
|
||||
this.startAnimation(index, item)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// Safely cancel all animations
|
||||
if (this.animationFrames) {
|
||||
Object.keys(this.animationFrames).forEach(index => {
|
||||
cancelAnimationFrame(this.animationFrames[index])
|
||||
})
|
||||
// 清除数组引用
|
||||
this.animatedValues = []
|
||||
this.formattedValues = []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
extractNumber(value) {
|
||||
if (value === null || value === undefined) return 0
|
||||
const match = String(value).match(/\d+\.?\d*/)
|
||||
return match ? parseFloat(match[0]) : 0
|
||||
},
|
||||
|
||||
formatNumber(num, prefix = '') {
|
||||
if (isNaN(num)) return prefix + '0'
|
||||
return prefix + Math.floor(num).toLocaleString()
|
||||
},
|
||||
|
||||
getValuePrefix(originalValue) {
|
||||
if (!originalValue) return ''
|
||||
const strVal = String(originalValue)
|
||||
if (strVal.includes('¥')) return '¥'
|
||||
if (strVal.includes('$')) return '$'
|
||||
return ''
|
||||
},
|
||||
|
||||
startAnimation(index, item) {
|
||||
// Add null checks
|
||||
if (!item || item.value === undefined) {
|
||||
this.formattedValues[index] = ''
|
||||
return
|
||||
}
|
||||
|
||||
const targetValue = this.extractNumber(item.value)
|
||||
const prefix = this.getValuePrefix(item.value)
|
||||
|
||||
// Check if animationFrames exists and has the index
|
||||
if (this.animationFrames && this.animationFrames[index] !== undefined) {
|
||||
cancelAnimationFrame(this.animationFrames[index])
|
||||
}
|
||||
|
||||
this.animateValue(index, targetValue, prefix)
|
||||
},
|
||||
|
||||
animateValue(index, targetValue, prefix) {
|
||||
const duration = 1000
|
||||
const startTime = performance.now()
|
||||
const startValue = 0
|
||||
|
||||
const animate = (timestamp) => {
|
||||
const elapsed = timestamp - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
const currentValue = Math.floor(progress * (targetValue - startValue) + startValue)
|
||||
|
||||
// Use Vue.set for arrays to ensure reactivity
|
||||
this.$set(this.animatedValues, index, currentValue)
|
||||
this.$set(this.formattedValues, index, this.formatNumber(currentValue, prefix))
|
||||
|
||||
if (progress < 1) {
|
||||
this.$set(this.animationFrames, index, requestAnimationFrame(animate))
|
||||
} else {
|
||||
// Ensure final values are set
|
||||
this.$set(this.animatedValues, index, targetValue)
|
||||
this.$set(this.formattedValues, index, this.formatNumber(targetValue, prefix))
|
||||
this.$delete(this.animationFrames, index)
|
||||
}
|
||||
}
|
||||
|
||||
this.$set(this.animationFrames, index, requestAnimationFrame(animate))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stat-row {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.stat-card {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
|
||||
.icon-wrapper {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-text {
|
||||
flex: 1;
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin: 0 0 5px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 22px;
|
||||
color: #303133;
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.trend {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
|
||||
i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.period {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.stat-row {
|
||||
.stat-card {
|
||||
.stat-content {
|
||||
padding: 12px;
|
||||
|
||||
.icon-wrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-text {
|
||||
.value {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画过渡效果 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,136 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<div class="dashboard-text">name: {{ name }}</div>
|
||||
<!-- 数据统计卡片 -->
|
||||
<stat-cards :stats-list="statsList" />
|
||||
|
||||
<!-- 快捷操作矩阵 -->
|
||||
<!-- <quick-actions-->
|
||||
<!-- :projects="projects"-->
|
||||
<!-- :actions="actions"-->
|
||||
<!-- @action-click="goTo"-->
|
||||
<!-- />-->
|
||||
<!-- 图表区 -->
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<el-col :xs="24" :md="16">
|
||||
<sales-trend-chart />
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="8">
|
||||
<order-distribution-chart />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 最新订单 -->
|
||||
<recent-orders
|
||||
:orders="recentOrders"
|
||||
@view-detail="viewOrder"
|
||||
@view-more="goTo('/orders')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import StatCards from './components/StatCards.vue'
|
||||
import QuickActions from './components/QuickActions.vue'
|
||||
import SalesTrendChart from './components/SalesTrendChart.vue'
|
||||
import OrderDistributionChart from './components/OrderDistributionChart.vue'
|
||||
import RecentOrders from './components/RecentOrders.vue'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'name'
|
||||
])
|
||||
components: {
|
||||
StatCards,
|
||||
QuickActions,
|
||||
SalesTrendChart,
|
||||
OrderDistributionChart,
|
||||
RecentOrders
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
statsList: [
|
||||
{
|
||||
label: '总用户数',
|
||||
value: '42345',
|
||||
icon: 'el-icon-user-solid',
|
||||
color: '#409EFF',
|
||||
trend: 12.5
|
||||
},
|
||||
{
|
||||
label: '总订单数',
|
||||
value: '890',
|
||||
icon: 'el-icon-tickets',
|
||||
color: '#F56C6C',
|
||||
trend: 8.2
|
||||
},
|
||||
{
|
||||
label: '本月销售额',
|
||||
value: '¥78900',
|
||||
icon: 'el-icon-money',
|
||||
color: '#67C23A',
|
||||
trend: -2.4
|
||||
},
|
||||
{
|
||||
label: '商品总数',
|
||||
value: '234',
|
||||
icon: 'el-icon-s-goods',
|
||||
color: '#E6A23C',
|
||||
trend: 5.6
|
||||
}
|
||||
],
|
||||
projects: [
|
||||
{ name: '天维服务管理系统', description: 'Version 2.1.0', icon: 'office-building', url: 'http://localhost:8080' },
|
||||
{ name: '星璇网关管理系统', description: 'Version 2.1.0', icon: 'office-building', url: 'http://localhost:8081' },
|
||||
{ name: '数据融合业务中台', description: 'Version 2.1.0', icon: 'office-building', url: 'http://localhost:8082' },
|
||||
{ name: '统一身份认证登录', description: 'Version 2.1.0', icon: 'office-building', url: 'http://localhost:8083' }
|
||||
|
||||
],
|
||||
actions: [
|
||||
{ label: '用户管理', icon: 'el-icon-user', path: '/users', color: '#409EFF' },
|
||||
{ label: '订单管理', icon: 'el-icon-tickets', path: '/orders', color: '#F56C6C' },
|
||||
{ label: '商品管理', icon: 'el-icon-s-goods', path: '/products', color: '#E6A23C' },
|
||||
{ label: '库存管理', icon: 'el-icon-box', path: '/inventory', color: '#909399' },
|
||||
{ label: '数据报表', icon: 'el-icon-data-line', path: '/reports', color: '#67C23A' },
|
||||
{ label: '系统设置', icon: 'el-icon-setting', path: '/settings', color: '#9C27B0' }
|
||||
],
|
||||
recentOrders: [
|
||||
{ id: 'A001', customer: '张三', product: 'iPhone 13 Pro', amount: 8999, status: '已完成', date: '2023-05-12' },
|
||||
{ id: 'A002', customer: '李四', product: 'MacBook Pro', amount: 12999, status: '处理中', date: '2023-05-11' },
|
||||
{ id: 'A003', customer: '王五', product: 'AirPods Pro', amount: 1499, status: '已完成', date: '2023-05-10' },
|
||||
{ id: 'A004', customer: '赵六', product: 'iPad Air', amount: 4399, status: '已发货', date: '2023-05-09' },
|
||||
{ id: 'A004', customer: '赵六', product: 'iPad Air', amount: 4399, status: '已发货', date: '2023-05-09' },
|
||||
{ id: 'A004', customer: '赵六', product: 'iPad Air', amount: 4399, status: '已发货', date: '2023-05-09' },
|
||||
{ id: 'A005', customer: '钱七', product: 'Apple Watch', amount: 2999, status: '待付款', date: '2023-05-08' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goTo(path) {
|
||||
this.$router.push(path)
|
||||
},
|
||||
viewOrder(order) {
|
||||
this.$message.success(`查看订单: ${order.id}`)
|
||||
// 实际项目中可以跳转到订单详情页
|
||||
// this.$router.push(`/orders/detail/${order.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
&-container {
|
||||
margin: 30px;
|
||||
}
|
||||
&-text {
|
||||
font-size: 30px;
|
||||
line-height: 46px;
|
||||
.dashboard-container {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chart-row {
|
||||
.el-col {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left">
|
||||
<el-form
|
||||
ref="loginForm"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
class="login-form"
|
||||
auto-complete="on"
|
||||
label-position="left"
|
||||
>
|
||||
<transition name="fade">
|
||||
<div class="title-container">
|
||||
<h3 class="title">CA 证书签发平台</h3>
|
||||
<h3 class="title">CA 证书签发管理系统</h3>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
@@ -14,7 +21,7 @@
|
||||
<el-input
|
||||
ref="username"
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名 / 邮箱 / 手机号"
|
||||
placeholder="用户名/邮箱/手机号"
|
||||
name="username"
|
||||
type="text"
|
||||
tabindex="1"
|
||||
@@ -42,11 +49,47 @@
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
|
||||
<!-- 新增:验证码 -->
|
||||
<!-- <el-form-item prop="captcha">-->
|
||||
<!-- <span class="svg-container">-->
|
||||
<!-- <svg-icon icon-class="validCode" />-->
|
||||
<!-- </span>-->
|
||||
<!-- <el-input-->
|
||||
<!-- v-model="loginForm.captcha"-->
|
||||
<!-- placeholder="验证码"-->
|
||||
<!-- name="captcha"-->
|
||||
<!-- style="width: 60%; display: inline-block"-->
|
||||
<!-- tabindex="3"-->
|
||||
<!-- />-->
|
||||
<!-- <!– <img :src="captchaUrl" alt="验证码" class="captcha-img" @click="refreshCaptcha" />–>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<!-- 新增:“记住我”选项 -->
|
||||
<el-checkbox v-model="loginForm.rememberMe" style="margin-bottom: 20px;">记住我</el-checkbox>
|
||||
<div>
|
||||
<div>
|
||||
|
||||
<el-button
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
style="width:100%;margin-bottom:30px;"
|
||||
@click.native.prevent="handleLogin"
|
||||
>登录
|
||||
</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
style="width:100%;margin-bottom:30px;"
|
||||
@click.native.prevent="handleOauthLogin"
|
||||
>统一认证登录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { validUsername } from '@/utils/validate'
|
||||
|
||||
@@ -67,10 +110,13 @@ export default {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loginForm: {
|
||||
username: 'admin',
|
||||
password: 'yyds@8848'
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
rememberMe: false
|
||||
},
|
||||
loginRules: {
|
||||
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
|
||||
@@ -78,7 +124,8 @@ export default {
|
||||
},
|
||||
loading: false,
|
||||
passwordType: 'password',
|
||||
redirect: undefined
|
||||
redirect: undefined,
|
||||
captchaUrl: '/api/captcha?timestamp=' + new Date().getTime() // 示例 URL
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -89,6 +136,16 @@ export default {
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 页面加载时检查是否已保存过信息
|
||||
const remembered = localStorage.getItem('rememberedUser')
|
||||
if (remembered) {
|
||||
const user = JSON.parse(remembered)
|
||||
this.loginForm.username = user.username
|
||||
this.loginForm.password = user.password
|
||||
this.loginForm.rememberMe = true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showPwd() {
|
||||
if (this.passwordType === 'password') {
|
||||
@@ -100,15 +157,31 @@ export default {
|
||||
this.$refs.password.focus()
|
||||
})
|
||||
},
|
||||
refreshCaptcha() {
|
||||
this.captchaUrl = '/api/captcha?timestamp=' + new Date().getTime()
|
||||
},
|
||||
handleOauthLogin() {
|
||||
this.$router.push({ path: '/social' })
|
||||
},
|
||||
handleLogin() {
|
||||
this.$refs.loginForm.validate(valid => {
|
||||
if (valid) {
|
||||
this.loading = true
|
||||
|
||||
// 如果用户勾选了“记住我”,保存账号密码到 localStorage
|
||||
if (this.loginForm.rememberMe) {
|
||||
localStorage.setItem('rememberedUser', JSON.stringify({
|
||||
username: this.loginForm.username,
|
||||
password: this.loginForm.password
|
||||
}))
|
||||
}
|
||||
|
||||
this.$store.dispatch('user/login', this.loginForm).then(() => {
|
||||
this.$router.push({ path: this.redirect || '/' })
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.refreshCaptcha()
|
||||
})
|
||||
} else {
|
||||
console.log('error submit!!')
|
||||
@@ -118,14 +191,15 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 修复input 背景不协调 和光标变色 */
|
||||
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
|
||||
|
||||
$bg:#283443;
|
||||
$light_gray:#fff;
|
||||
$bg: #283443;
|
||||
$light_gray: #fff;
|
||||
$cursor: #fff;
|
||||
|
||||
@supports (-webkit-mask: none) and (not (caret-color: $cursor)) {
|
||||
@@ -143,16 +217,17 @@ $cursor: #fff;
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
border: 0;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0px;
|
||||
appearance: none; // 新增标准属性
|
||||
border-radius: 0;
|
||||
padding: 12px 5px 12px 15px;
|
||||
color: $light_gray;
|
||||
height: 47px;
|
||||
caret-color: $cursor;
|
||||
|
||||
&:-webkit-autofill {
|
||||
box-shadow: 0 0 0px 1000px $bg inset !important;
|
||||
box-shadow: 0 0 0 1000px $bg inset !important;
|
||||
-webkit-text-fill-color: $cursor !important;
|
||||
}
|
||||
}
|
||||
@@ -169,8 +244,8 @@ $cursor: #fff;
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$bg: linear-gradient(135deg, #1e3c72, #2a5298); // 渐变蓝背景
|
||||
$dark_gray:#666;
|
||||
$light_gray:#fff;
|
||||
$dark_gray: #666;
|
||||
$light_gray: #fff;
|
||||
|
||||
.login-container {
|
||||
min-height: 100%;
|
||||
@@ -246,10 +321,27 @@ $light_gray:#fff;
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-img {
|
||||
width: 90px;
|
||||
height: 40px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
// 可以给验证码区域加点样式
|
||||
.el-form-item__content .svg-container {
|
||||
width: 30px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
// 过渡动画
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
28
ca-admin-ui/src/views/login/social/callback.vue
Normal file
28
ca-admin-ui/src/views/login/social/callback.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
this.handleGetAccessToken()
|
||||
},
|
||||
methods: {
|
||||
handleGetAccessToken() {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const authorizationCode = urlParams.get('code')
|
||||
const state = urlParams.get('state')
|
||||
console.log(authorizationCode)
|
||||
this.$store.dispatch('user/callback', authorizationCode).then(() => {
|
||||
this.$router.push({ path: this.redirect || '/' })
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.refreshCaptcha()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
28
ca-admin-ui/src/views/login/social/index.vue
Normal file
28
ca-admin-ui/src/views/login/social/index.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script>
|
||||
import { buildAuthorizeUrl } from '@/utils/oauth-utils'
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
this.handleAuthorize()
|
||||
},
|
||||
methods: {
|
||||
handleAuthorize() {
|
||||
const authUrl = buildAuthorizeUrl(
|
||||
'http://127.0.0.1:8080',
|
||||
'certificate-authority-client',
|
||||
'http://127.0.0.1:9529/callback',
|
||||
'',
|
||||
'openid profile certificate.read certificate.write'
|
||||
)
|
||||
console.log(authUrl)
|
||||
debugger
|
||||
// 跳转授权页面(可选)
|
||||
window.location.href = authUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -6,14 +6,14 @@ function resolve(dir) {
|
||||
return path.join(__dirname, dir)
|
||||
}
|
||||
|
||||
const name = defaultSettings.title || 'CA 证书签发平台 ' // page title
|
||||
const name = defaultSettings.title || 'CA 证书签发管理系统 ' // page title
|
||||
|
||||
// If your port is set to 80,
|
||||
// use administrator privileges to execute the command line.
|
||||
// For example, Mac: sudo npm run
|
||||
// You can change the port by the following methods:
|
||||
// port = 9528 npm run dev OR npm run dev --port = 9528
|
||||
const port = process.env.port || process.env.npm_config_port || 9528 // dev port
|
||||
const port = process.env.port || process.env.npm_config_port || 9529 // dev port
|
||||
|
||||
// All configuration item explanations can be find in https://cli.vuejs.org/config/
|
||||
module.exports = {
|
||||
@@ -24,7 +24,8 @@ module.exports = {
|
||||
* In most cases please use '/' !!!
|
||||
* Detail: https://cli.vuejs.org/config/#publicpath
|
||||
*/
|
||||
publicPath: '/ca-admin',
|
||||
// publicPath: '/ca-admin',
|
||||
publicPath: '/',
|
||||
outputDir: 'dist',
|
||||
assetsDir: 'static',
|
||||
lintOnSave: process.env.NODE_ENV === 'development',
|
||||
@@ -36,7 +37,7 @@ module.exports = {
|
||||
warnings: false,
|
||||
errors: true
|
||||
},
|
||||
before: require('./mock/mock-server.js'),
|
||||
// before: require('./mock/mock-server.js'),
|
||||
proxy: {
|
||||
[process.env.VUE_APP_BASE_API]: {
|
||||
target: process.env.VUE_APP_PROXY_API,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user