对接统一登录认证

This commit is contained in:
wangjianhong
2025-07-23 22:17:47 +08:00
parent 5e4e272b3a
commit e7442ae419
48 changed files with 2940 additions and 3067 deletions

View File

@@ -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/'

View File

@@ -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",

View File

@@ -22,3 +22,11 @@ export function logout() {
method: 'post'
})
}
export function callback(code) {
return request({
url: '/user/oauth/login/callback',
method: 'get',
params: { code }
})
}

View File

@@ -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()

View File

@@ -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
})

View File

@@ -1,6 +1,6 @@
module.exports = {
title: 'CA 证书签发平台',
title: 'CA 证书签发管理系统',
/**
* @type {boolean} true | false

View File

@@ -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)

View 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()}`;
}

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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"-->
<!-- />-->
<!-- &lt;!&ndash; <img :src="captchaUrl" alt="验证码" class="captcha-img" @click="refreshCaptcha" />&ndash;&gt;-->
<!-- </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;
}

View 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>

View 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>

View File

@@ -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