refactor: 添加核销页面

This commit is contained in:
sunxiaolong
2024-07-01 00:02:26 +08:00
parent c5d944d790
commit 3808154e04
17 changed files with 221 additions and 168 deletions

5
.hintrc Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": [
"development"
]
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arco Design Pro - 开箱即用的中台前端/设计解决方案</title>
<title>卡销解决终端方案</title>
</head>
<body>
<div id="app"></div>

View File

@@ -15,6 +15,11 @@ export interface AppleCardRecord extends AppleCardAddRecord {
todayRechargeCount: number;
todayRechargeDatetime: string;
balance: number;
uploadUser: {
id: string;
username: string;
userNickname: string;
};
}
export interface AppleCardParams

View File

@@ -6,6 +6,8 @@ import { encryptWithBase64 } from '@/utils/encrypt';
export interface LoginData {
username: string;
password: string;
verifyCode: string;
verifyKey: string;
}
export interface LoginRes {
@@ -14,23 +16,23 @@ export interface LoginRes {
export function login(data: LoginData) {
return axios.post<LoginRes>('/user/login', {
username: data.username,
password: encryptWithBase64(data.password)
password: encryptWithBase64(data.password),
...data
});
}
export function logout() {
return axios.post<LoginRes>('/api/user/logout');
return axios.post<LoginRes>('/user/logout');
}
export function getUserInfo() {
return axios.post<UserState>('/api/user/info');
return axios.post<UserState>('/user/info');
}
export function getMenuList() {
return axios.post<RouteRecordNormalized[]>('/api/user/menu');
return axios.post<RouteRecordNormalized[]>('/user/menu');
}
export function getCapchaAPI() {
return axios.get('/api/capcha');
return axios.get<{ key: string; img: string }>('/captcha');
}

View File

@@ -6,8 +6,8 @@ html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
margin: 0;
font-size: 14px;
background-color: var(--color-bg-1);
-moz-osx-font-smoothing: grayscale;
@@ -17,61 +17,70 @@ body {
.echarts-tooltip-diy {
background: linear-gradient(
304.17deg,
rgba(253, 254, 255, 0.6) -6.04%,
rgba(244, 247, 252, 0.6) 85.2%
rgb(253 254 255 / 60%) -6.04%,
rgb(244 247 252 / 60%) 85.2%
) !important;
border: none !important;
backdrop-filter: blur(10px) !important;
backdrop-filter: blur(10px) !important;
border: none !important;
/* Note: backdrop-filter has minimal browser support */
border-radius: 6px !important;
.content-panel {
display: flex;
justify-content: space-between;
padding: 0 9px;
background: rgba(255, 255, 255, 0.8);
width: 164px;
height: 32px;
line-height: 32px;
box-shadow: 6px 0px 20px rgba(34, 87, 188, 0.1);
border-radius: 4px;
padding: 0 9px;
margin-bottom: 4px;
line-height: 32px;
background: rgb(255 255 255 / 80%);
border-radius: 4px;
box-shadow: 6px 0 20px rgb(34 87 188 / 10%);
}
.tooltip-title {
margin: 0 0 10px 0;
margin: 0 0 10px;
}
p {
margin: 0;
}
.tooltip-title,
.tooltip-value {
font-size: 13px;
line-height: 15px;
display: flex;
align-items: center;
text-align: right;
color: #1d2129;
font-size: 13px;
font-weight: bold;
line-height: 15px;
color: #1d2129;
text-align: right;
}
.tooltip-item-icon {
display: inline-block;
margin-right: 8px;
width: 10px;
height: 10px;
margin-right: 8px;
border-radius: 50%;
}
}
.general-card {
border-radius: 4px;
border: none;
border-radius: 4px;
& > .arco-card-header {
height: auto;
padding: 20px;
border: none;
}
& > .arco-card-body {
padding: 0 20px 20px 20px;
padding: 0 20px 20px;
}
}
@@ -82,11 +91,12 @@ body {
.arco-table-cell {
.circle {
display: inline-block;
margin-right: 4px;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 4px;
background-color: rgb(var(--blue-6));
border-radius: 50%;
&.pass {
background-color: rgb(var(--green-6));
}

View File

@@ -1,57 +1,57 @@
@import './breakpoint.less';
/******** size *******/
// /******** size *******/
// @size-mini: 12px;
// @size-small: 13px;
// @size-default: 14px;
// @size-large: 16px;
// // @size-mini: 12px;
// // @size-small: 13px;
// // @size-default: 14px;
// // @size-large: 16px;
/******** borderSize *******/
// /******** borderSize *******/
@border-none: 0;
@border-1: 1px;
@border-2: 2px;
@border-3: 3px;
@border-4: 4px;
@border-5: 5px;
// @border-none: 0;
// @border-1: 1px;
// @border-2: 2px;
// @border-3: 3px;
// @border-4: 4px;
// @border-5: 5px;
/******** borderStyle *******/
// /******** borderStyle *******/
@border-solid: solid;
@border-dashed: dashed;
@border-dotted: dotted;
// @border-solid: solid;
// @border-dashed: dashed;
// @border-dotted: dotted;
/******** radius *******/
// /******** radius *******/
@border-radius-none: 0;
@border-radius-small: 2px;
@border-radius-medium: 4px;
@border-radius-large: 8px;
@border-radius-circle: 50%;
// @border-radius-none: 0;
// @border-radius-small: 2px;
// @border-radius-medium: 4px;
// @border-radius-large: 8px;
// @border-radius-circle: 50%;
// /******** 间距 2*N *******/
// // /******** 间距 2*N *******/
@spacing-0: 0;
@spacing-1: 2px;
@spacing-2: 4px;
@spacing-3: 6px;
@spacing-4: 8px;
@spacing-5: 10px;
@spacing-6: 12px;
@spacing-7: 16px;
@spacing-8: 20px;
@spacing-9: 24px;
@spacing-10: 32px;
@spacing-11: 36px;
@spacing-12: 40px;
@spacing-13: 48px;
@spacing-14: 56px;
@spacing-15: 60px;
@spacing-16: 64px;
@spacing-17: 72px;
@spacing-18: 80px;
@spacing-19: 84px;
@spacing-20: 96px;
@spacing-21: 100px;
@spacing-22: 120px;
// @spacing-0: 0;
// @spacing-1: 2px;
// @spacing-2: 4px;
// @spacing-3: 6px;
// @spacing-4: 8px;
// @spacing-5: 10px;
// @spacing-6: 12px;
// @spacing-7: 16px;
// @spacing-8: 20px;
// @spacing-9: 24px;
// @spacing-10: 32px;
// @spacing-11: 36px;
// @spacing-12: 40px;
// @spacing-13: 48px;
// @spacing-14: 56px;
// @spacing-15: 60px;
// @spacing-16: 64px;
// @spacing-17: 72px;
// @spacing-18: 80px;
// @spacing-19: 84px;
// @spacing-20: 96px;
// @spacing-21: 100px;
// @spacing-22: 120px;

View File

@@ -78,25 +78,35 @@
</li> -->
<li>
<a-dropdown trigger="click">
<a-avatar
:style="{
backgroundColor: '#3370ff',
marginRight: '8px',
cursor: 'pointer'
}"
:size="32"
>
用户
</a-avatar>
<template #content>
<a-doption>
<!-- <a-doption>
<a-space @click="switchRoles">
<icon-tag />
<span>切换角色</span>
</a-space>
</a-doption>
<a-doption>
</a-doption> -->
<!-- <a-doption>
<a-space @click="$router.push({ name: 'Info' })">
<icon-user />
<span>用户中心</span>
</a-space>
</a-doption>
<a-doption>
</a-doption> -->
<!-- <a-doption>
<a-space @click="$router.push({ name: 'Setting' })">
<icon-settings />
<span>用户设置</span>
</a-space>
</a-doption>
</a-doption> -->
<a-doption>
<a-space @click="handleLogout">
<icon-export />

View File

@@ -1,16 +1,19 @@
import { isUndefined } from '@/utils/is';
import { appRoutes, appExternalRoutes } from '../routes';
const mixinRoutes = [...appRoutes, ...appExternalRoutes];
const appClientMenus = mixinRoutes.map(el => {
const { name, path, meta, redirect, children } = el;
return {
name,
path,
meta,
redirect,
children
};
});
const appClientMenus = mixinRoutes
.filter(el => isUndefined(el.meta.showInMenu))
.map(el => {
const { name, path, meta, redirect, children } = el;
return {
name,
path,
meta,
redirect,
children
};
});
export default appClientMenus;

View File

@@ -0,0 +1,28 @@
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const RECHARGEINFO: AppRouteRecordRaw = {
path: '/rechargeInfo',
name: 'rechargeInfo',
component: DEFAULT_LAYOUT,
meta: {
locale: '充值详情',
requiresAuth: true,
icon: 'icon-safe',
order: 1
},
children: [
{
path: 'appleAccount',
name: 'appleAccount',
component: () => import('@/views/apple-card-info/card-info/index.vue'),
meta: {
locale: '苹果账户',
requiresAuth: true,
roles: ['*']
}
}
]
};
export default RECHARGEINFO;

View File

@@ -9,7 +9,8 @@ const DASHBOARD: AppRouteRecordRaw = {
locale: '仪表盘',
requiresAuth: true,
icon: 'icon-dashboard',
order: 0
order: 0,
showInMenu: false
},
children: [
{

View File

@@ -7,6 +7,7 @@ const IFRAME: AppRouteRecordRaw = {
component: IFRAME_LAYOUT,
meta: {
locale: '外链',
showInMenu: false,
requiresAuth: false
},
children: [
@@ -21,7 +22,7 @@ const IFRAME: AppRouteRecordRaw = {
},
{
path: 'appleAccount',
name: 'AppleAccount',
name: 'iframeAppleAccount',
component: () => import('@/views/apple-card-info/card-info/index.vue'),
meta: {
locale: '账号(苹果)',
@@ -30,7 +31,7 @@ const IFRAME: AppRouteRecordRaw = {
},
{
path: 'rechargeOrder',
name: 'RechargeOrder',
name: 'iframeRechargeOrder',
component: () =>
import('@/views/apple-card-info/recharge-history/index.vue'),
meta: {
@@ -40,7 +41,7 @@ const IFRAME: AppRouteRecordRaw = {
},
{
path: 'orderSummary',
name: 'OrderSummary',
name: 'iframeOrderSummary',
component: () =>
import('@/views/apple-card-info/order-summary/index.vue'),
meta: {

View File

@@ -1,28 +0,0 @@
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const MERCHANT: AppRouteRecordRaw = {
path: '/merchant',
name: 'merchant',
component: DEFAULT_LAYOUT,
meta: {
locale: '商户管理',
requiresAuth: true,
icon: 'icon-dashboard',
order: 0
},
children: [
{
path: 'merchantConfig',
name: 'MerchantConfig',
component: () => import('@/views/merchant-config/index.vue'),
meta: {
locale: '商户配置',
requiresAuth: true,
roles: ['*']
}
}
]
};
export default MERCHANT;

View File

@@ -6,6 +6,7 @@ const DASHBOARD: AppRouteRecordRaw = {
name: 'userCenter',
component: DEFAULT_LAYOUT,
meta: {
showInMenu: false,
locale: '用户管理',
requiresAuth: true,
icon: 'icon-dashboard',

View File

@@ -12,5 +12,6 @@ declare module 'vue-router' {
order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is
noAffix?: boolean; // if set true, the tag will not affix in the tab-bar
ignoreCache?: boolean; // if set true, the page will not be cached
showInMenu?: boolean;
}
}

View File

@@ -76,6 +76,7 @@
:columns="columns"
:data="renderData"
:bordered="false"
:scroll="{ x: 1000 }"
column-resizable:bordered="{cell:true}"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@@ -189,6 +190,11 @@ const columns: TableColumnData[] = [
dataIndex: 'createdAt',
slotName: 'createdAt'
},
{
title: '上传人',
dataIndex: 'uploadUser.username',
slotName: 'uploadUser'
},
{
title: '状态',
dataIndex: 'status',
@@ -197,7 +203,9 @@ const columns: TableColumnData[] = [
{
title: '操作',
dataIndex: 'operations',
slotName: 'operations'
slotName: 'operations',
fixed: 'right',
width: 200
}
];
const generateFormModel = () => {

View File

@@ -7,7 +7,7 @@
:rules="rules"
>
<div class="login-form-title">欢迎登录</div>
<a-tabs v-model:active-key="tabActiveKey" size="mini" animation>
<a-tabs v-model:active-key="tabActiveKey" animation>
<a-tab-pane key="1" title="账号登录" destroy-on-hide>
<a-form-item field="username" validate-trigger="blur" hide-label>
<a-input
@@ -33,8 +33,8 @@
</a-input-password>
</a-form-item>
<a-form-item field="capcha" validate-trigger="blur" hide-label>
<a-row justify="space-around">
<a-col :span="8">
<a-row justify="space-around" :gutter="8">
<a-col :span="16">
<a-input
v-model="form.captcha"
placeholder="请输入验证码"
@@ -45,7 +45,7 @@
</template>
</a-input>
</a-col>
<a-col :span="3">
<a-col :span="8">
<img
:src="state.captchaUrl"
alt=""
@@ -143,7 +143,8 @@ const formRef = ref();
const tabActiveKey = ref('1');
const state = reactive({
// 验证码
captchaUrl: ''
captchaUrl: '',
captchaKey: ''
});
const { loading, setLoading } = useLoading();
@@ -157,21 +158,21 @@ const form = reactive({
username: loginConfig.value.username,
password: loginConfig.value.password,
// phone: '',
captcha: '',
agreement: false
captcha: ''
// agreement: false
});
const rules = {
username: [{ required: true, message: '请输入正确账号' }],
captcha: [{ required: true, message: '请输入正确验证码' }],
password: [
{ required: true, message: '请输入密码' },
{
// 密码格式6-32位包含大小写字母、数字、特殊字符(除空格)两种以上
match:
/^(?![\d]+$)(?![a-z]+$)(?![A-Z]+$)(?![~!@#$%^&*.]+$)[\da-zA-z~!@#$%^&*.]{6,32}$/,
message: '密码格式不正确'
}
{ required: true, message: '请输入密码' }
// {
// // 密码格式6-32位包含大小写字母、数字、特殊字符(除空格)两种以上
// match:
// /^(?![\d]+$)(?![a-z]+$)(?![A-Z]+$)(?![~!@#$%^&*.]+$)[\da-zA-z~!@#$%^&*.]{6,32}$/,
// message: '密码格式不正确'
// }
]
// phone: [
// { required: true, message: '请输入手机号' },
@@ -183,41 +184,46 @@ const handleSubmit = () => {
if (loading.value) return;
if (tabActiveKey.value === '1') {
formRef.value.validateField(['username', 'password']).then(async res => {
if (res) return;
if (!form.agreement) {
return Message.info('请阅读并同意服务协议和隐私政策');
}
setLoading(true);
try {
const userInfoForm = pick(form, ['username', 'password']);
console.log(userInfoForm);
await userStore.login(userInfoForm as LoginData);
const { redirect, ...othersQuery } = router.currentRoute.value.query;
router.push({
name: (redirect as string) || 'Workplace',
query: {
...othersQuery
}
});
Message.success('登录成功');
const { rememberPassword } = loginConfig.value;
const { username, password } = userInfoForm;
// 实际生产环境需要进行加密存储。
loginConfig.value.username = rememberPassword ? username : '';
loginConfig.value.password = rememberPassword ? password : '';
} finally {
setLoading(false);
}
});
formRef.value
.validateField(['username', 'password', 'captcha'])
.then(async res => {
if (res) return;
// if (!form.agreement) {
// return Message.info('请阅读并同意服务协议和隐私政策');
// }
setLoading(true);
try {
const userInfoForm = pick(form, ['username', 'captcha', 'password']);
await userStore.login({
verifyKey: state.captchaKey,
verifyCode: userInfoForm.captcha,
username: userInfoForm.username,
password: userInfoForm.password
});
const { redirect, ...othersQuery } = router.currentRoute.value.query;
router.push({
name: (redirect as string) || 'Workplace',
query: {
...othersQuery
}
});
Message.success('登录成功');
const { rememberPassword } = loginConfig.value;
const { username, password } = userInfoForm;
// 实际生产环境需要进行加密存储。
loginConfig.value.username = rememberPassword ? username : '';
loginConfig.value.password = rememberPassword ? password : '';
} finally {
setLoading(false);
}
});
}
if (tabActiveKey.value === '2') {
formRef.value.validateField(['phone', 'captcha']).then(res => {
if (res) return;
if (!form.agreement)
return Message.info('请阅读并同意服务协议和隐私政策');
// if (!form.agreement)
// return Message.info('请阅读并同意服务协议和隐私政策');
// setLoading(true);
});
}
@@ -229,7 +235,8 @@ const setRememberPassword = (value: boolean) => {
const getCaptcha = async () => {
const res = await getCapchaAPI();
state.captchaUrl = res.data.url;
state.captchaUrl = res.data.img;
state.captchaKey = res.data.key;
};
onMounted(() => {
@@ -311,6 +318,6 @@ onMounted(() => {
}
:deep(.arco-tabs-content) {
height: 155px;
height: 192px;
}
</style>

View File

@@ -47,7 +47,6 @@ import LogoSvg from '@/assets/logo.svg';
display: inline-flex;
align-items: center;
width: 100%;
padding: @spacing-7 @spacing-0;
&-text {
margin-right: 4px;