wip:个人中心&代理用户详情页调整
This commit is contained in:
parent
3a4f3f1df1
commit
b694fb3461
|
|
@ -1,6 +1,7 @@
|
||||||
//获取代理客户列表接口
|
// 获取代理客户详情接口
|
||||||
import { request } from '@/api';
|
import { request } from '@/api';
|
||||||
|
import type { GetCustomerDetailParams, CustomerDetailResponse } from '@/api/types/agent';
|
||||||
|
|
||||||
export const getCustomerDetailApi = (params: any) => {
|
export const getCustomerDetailApi = (params: GetCustomerDetailParams): Promise<CustomerDetailResponse> => {
|
||||||
return request.get('/agent/getAgentUserDetail', {...params})
|
return request.get<CustomerDetailResponse>('/agent/getAgentUserDetail', params);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,39 @@
|
||||||
import { request } from '@/api';
|
import { request } from '@/api';
|
||||||
|
import type {
|
||||||
|
UserInfo,
|
||||||
|
EditLangTypeParams,
|
||||||
|
BindMobileParams,
|
||||||
|
BindEmailParams,
|
||||||
|
EditUsernameParams,
|
||||||
|
UpdatePwdByCaptchaParams
|
||||||
|
} from '@/api/types/profile';
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
export async function getUserInfoApi() {
|
export async function getUserInfoApi(): Promise<UserInfo> {
|
||||||
return request.get('/user/getUserInfo');
|
return request.get<UserInfo>('/user/getUserInfo');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改语言类型
|
// 修改语言类型
|
||||||
export async function editLangTypeApi(lang_type: number) {
|
export async function editLangTypeApi(params: EditLangTypeParams): Promise<void> {
|
||||||
return request.post('/user/editLangType', { lang_type });
|
return request.post('/user/editLangType', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定手机号
|
// 绑定手机号
|
||||||
export async function bindMobileApi(data: { mobile: string; code: string }) {
|
export async function bindMobileApi(data: BindMobileParams): Promise<void> {
|
||||||
return request.post('/user/bindMobile', data);
|
return request.post('/user/bindMobile', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定邮箱
|
// 绑定邮箱
|
||||||
export async function bindEmailApi(data: { email: string; code: string }) {
|
export async function bindEmailApi(data: BindEmailParams): Promise<void> {
|
||||||
return request.post('/user/bindEmail', data);
|
return request.post('/user/bindEmail', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改用户名
|
// 修改用户名
|
||||||
export async function editUsernameApi(data: { user_id: number; username: string }) {
|
export async function editUsernameApi(data: EditUsernameParams): Promise<void> {
|
||||||
return request.post('/user/editUser', data);
|
return request.post('/user/editUser', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 短信验证码修改密码
|
|
||||||
// 验证码修改密码(统一接口)
|
// 验证码修改密码(统一接口)
|
||||||
export async function updatePwdByCaptchaApi(data: {
|
export async function updatePwdByCaptchaApi(data: UpdatePwdByCaptchaParams): Promise<void> {
|
||||||
reg_type: number;
|
|
||||||
email?: string;
|
|
||||||
mobile?: string;
|
|
||||||
code: string;
|
|
||||||
login_password1: string;
|
|
||||||
login_password2: string;
|
|
||||||
}) {
|
|
||||||
return request.post('/user/editLoginPass', data);
|
return request.post('/user/editLoginPass', data);
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
// 代理客户详情相关类型定义
|
||||||
|
|
||||||
|
// 角色映射类型
|
||||||
|
export type RoleType = 1 | 2 | 3 | 4 | 5 | 6
|
||||||
|
|
||||||
|
// 角色名称映射
|
||||||
|
export const ROLE_MAP: Record<RoleType, string> = {
|
||||||
|
1: '平台',
|
||||||
|
2: '承包商',
|
||||||
|
3: '特殊做市商',
|
||||||
|
4: '普通做市商',
|
||||||
|
5: '代理',
|
||||||
|
6: '用户',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户信息类型
|
||||||
|
export interface CustomerUserInfo {
|
||||||
|
id: string | number
|
||||||
|
email: string
|
||||||
|
mobile: string
|
||||||
|
role_id: RoleType
|
||||||
|
capital_amount: string | number
|
||||||
|
total_recharge_amount: string | number
|
||||||
|
history_transfer_amount: string | number
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交易账户钱包信息类型
|
||||||
|
export interface AccountWallet {
|
||||||
|
amount: string | number
|
||||||
|
lever: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交易账户类型
|
||||||
|
export interface Account {
|
||||||
|
account_id: string | number
|
||||||
|
account_name: string
|
||||||
|
wallets_trade: AccountWallet
|
||||||
|
collect_data: string | number
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户详情响应类型
|
||||||
|
export interface CustomerDetailResponse {
|
||||||
|
user: CustomerUserInfo
|
||||||
|
accounts: Account[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取客户详情参数类型
|
||||||
|
export interface GetCustomerDetailParams {
|
||||||
|
user_id: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格列配置类型
|
||||||
|
export interface TableColumn {
|
||||||
|
label: string
|
||||||
|
prop: string
|
||||||
|
width?: number
|
||||||
|
sortable?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户展示信息类型
|
||||||
|
export interface UserDisplayInfo {
|
||||||
|
name: string
|
||||||
|
id: string | number
|
||||||
|
role: string
|
||||||
|
phone: string
|
||||||
|
email: string
|
||||||
|
deposit: string | number
|
||||||
|
balance: string | number
|
||||||
|
registerTime: string
|
||||||
|
hisMeny: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格数据行类型
|
||||||
|
export interface TableDataRow {
|
||||||
|
tradeAccount: string | number
|
||||||
|
accountName: string
|
||||||
|
balance: string | number
|
||||||
|
leverage: string | number
|
||||||
|
tradeVolume: string | number
|
||||||
|
addTime: string
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
// 个人中心相关类型定义
|
||||||
|
|
||||||
|
// 用户信息类型
|
||||||
|
export interface UserInfo {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
mobile: string
|
||||||
|
lang_type: number
|
||||||
|
avatar: string
|
||||||
|
last_login: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑表单类型
|
||||||
|
export interface EditForm {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码表单类型
|
||||||
|
export interface PasswordForm {
|
||||||
|
verifyType: 'mobile' | 'email'
|
||||||
|
code: string
|
||||||
|
login_password1: string
|
||||||
|
login_password2: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改语言参数类型
|
||||||
|
export interface EditLangTypeParams {
|
||||||
|
lang_type: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定手机号参数类型
|
||||||
|
export interface BindMobileParams {
|
||||||
|
mobile: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定邮箱参数类型
|
||||||
|
export interface BindEmailParams {
|
||||||
|
email: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改用户名参数类型
|
||||||
|
export interface EditUsernameParams {
|
||||||
|
user_id: number
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码修改密码参数类型
|
||||||
|
export interface UpdatePwdByCaptchaParams {
|
||||||
|
reg_type: number
|
||||||
|
email?: string
|
||||||
|
mobile?: string
|
||||||
|
code: string
|
||||||
|
login_password1: string
|
||||||
|
login_password2: string
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<span>{{ animatedValue }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: number;
|
||||||
|
duration?: number;
|
||||||
|
decimals?: number;
|
||||||
|
separator?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
duration: 1000,
|
||||||
|
decimals: 2,
|
||||||
|
separator: ','
|
||||||
|
});
|
||||||
|
|
||||||
|
const animatedValue = ref('0.00');
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
let startTime: number | null = null;
|
||||||
|
const startValue = ref(0);
|
||||||
|
|
||||||
|
// 格式化数字
|
||||||
|
const formatNumber = (num: number): string => {
|
||||||
|
const fixed = num.toFixed(props.decimals);
|
||||||
|
const parts = fixed.split('.');
|
||||||
|
parts[0] && (parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, props.separator));
|
||||||
|
return parts.join('.');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 动画函数
|
||||||
|
const animate = (timestamp: number) => {
|
||||||
|
if (!startTime) startTime = timestamp;
|
||||||
|
const elapsed = timestamp - startTime;
|
||||||
|
const progress = Math.min(elapsed / props.duration, 1);
|
||||||
|
|
||||||
|
// 使用缓动函数 easeOutQuart
|
||||||
|
const easeProgress = 1 - Math.pow(1 - progress, 4);
|
||||||
|
|
||||||
|
const currentValue = startValue.value + (props.value - startValue.value) * easeProgress;
|
||||||
|
animatedValue.value = formatNumber(currentValue);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始动画
|
||||||
|
const startAnimation = () => {
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
startTime = null;
|
||||||
|
startValue.value = parseFloat(animatedValue.value.replace(/,/g, '')) || 0;
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听值变化
|
||||||
|
watch(() => props.value, () => {
|
||||||
|
startAnimation();
|
||||||
|
}, { immediate: false });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
startAnimation();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -34,7 +34,9 @@
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>账户余额</span>
|
<span>账户余额</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value text-highlight">{{ userInfo.balance || '0' }}</div>
|
<div class="item-value text-highlight">
|
||||||
|
<AnimatedNumber :value="Number(userInfo.balance) || 0" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<div class="item-label">
|
<div class="item-label">
|
||||||
|
|
@ -43,7 +45,9 @@
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>存款金额</span>
|
<span>存款金额</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value text-highlight">{{ userInfo.deposit || '0' }}</div>
|
<div class="item-value text-highlight">
|
||||||
|
<AnimatedNumber :value="Number(userInfo.deposit) || 0" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<div class="item-label">
|
<div class="item-label">
|
||||||
|
|
@ -70,7 +74,9 @@
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>历史交易金额</span>
|
<span>历史交易金额</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value">{{ userInfo.hisMeny || '-' }}</div>
|
<div class="item-value">
|
||||||
|
<AnimatedNumber :value="Number(userInfo.hisMeny) || 0" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -92,8 +98,20 @@ import { UserFilled, Phone, Message, Money, Wallet, Clock, CreditCard } from '@e
|
||||||
import CustomerListTable from '@/components/common/Table.vue'
|
import CustomerListTable from '@/components/common/Table.vue'
|
||||||
import { getCustomerDetailApi } from '@/api/modules/agent/customerDetail'
|
import { getCustomerDetailApi } from '@/api/modules/agent/customerDetail'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import AnimatedNumber from './AnimatedNumber.vue'
|
||||||
|
|
||||||
const roleMap: { [key: number]: string } = {
|
// 类型定义
|
||||||
|
import type {
|
||||||
|
RoleType,
|
||||||
|
CustomerUserInfo,
|
||||||
|
Account,
|
||||||
|
CustomerDetailResponse,
|
||||||
|
UserDisplayInfo,
|
||||||
|
TableDataRow,
|
||||||
|
TableColumn
|
||||||
|
} from '@/api/types/agent'
|
||||||
|
|
||||||
|
const roleMap: Record<RoleType, string> = {
|
||||||
1: '平台',
|
1: '平台',
|
||||||
2: '承包商',
|
2: '承包商',
|
||||||
3: '特殊做市商',
|
3: '特殊做市商',
|
||||||
|
|
@ -129,21 +147,21 @@ const loading = ref(false)
|
||||||
const tableLoading = ref(false)
|
const tableLoading = ref(false)
|
||||||
|
|
||||||
// 客户信息
|
// 客户信息
|
||||||
const userInfo = ref({
|
const userInfo = ref<UserDisplayInfo>({
|
||||||
name: '',
|
name: '',
|
||||||
id: '',
|
id: '',
|
||||||
role: '',
|
role: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
email: '',
|
email: '',
|
||||||
deposit: '',
|
deposit: 0,
|
||||||
balance: '',
|
balance: 0,
|
||||||
registerTime: '',
|
registerTime: '',
|
||||||
hisMeny: 122,
|
hisMeny: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 表格
|
// 表格
|
||||||
const tableData = ref()
|
const tableData = ref<TableDataRow[]>([])
|
||||||
const tableColumnsOptions = ref([
|
const tableColumnsOptions = ref<TableColumn[]>([
|
||||||
{ label: '交易账号', prop: 'tradeAccount' },
|
{ label: '交易账号', prop: 'tradeAccount' },
|
||||||
{ label: '账户名称', prop: 'accountName' },
|
{ label: '账户名称', prop: 'accountName' },
|
||||||
{ label: '余额', prop: 'balance', sortable: true },
|
{ label: '余额', prop: 'balance', sortable: true },
|
||||||
|
|
@ -153,40 +171,38 @@ const tableColumnsOptions = ref([
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
// ===================== 加载详情数据(替换你的接口) =====================
|
// ===================== 加载详情数据 =====================
|
||||||
const loadDetailData = async (id: string | number) => {
|
const loadDetailData = async (id: string | number) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
tableLoading.value = true
|
tableLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 这里替换成你的真实客户详情接口
|
const res: CustomerDetailResponse = await getCustomerDetailApi({ user_id: id });
|
||||||
getCustomerDetailApi({ user_id: id }).then((res) => {
|
const { accounts, user } = res;
|
||||||
console.log('res:', res);
|
|
||||||
const { accounts, user } = res;
|
userInfo.value = {
|
||||||
userInfo.value = {
|
name: user.email || user.mobile || '-',
|
||||||
name: user.email || user.mobile,
|
id: user.id,
|
||||||
id: user.id,
|
role: roleMap[user.role_id] || '-',
|
||||||
role: roleMap[user.role_id],
|
phone: user.mobile || '-',
|
||||||
phone: user.mobile || '-',
|
email: user.email || '-',
|
||||||
email: user.email || '-',
|
balance: Number(user.capital_amount) || 0,
|
||||||
balance: user.capital_amount,
|
deposit: Number(user.total_recharge_amount) || 0,
|
||||||
deposit: user.total_recharge_amount,
|
registerTime: dayjs(user.created_at).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
registerTime: dayjs(user.created_at).format('YYYY-MM-DD HH:mm:ss'),
|
hisMeny: Number(user.history_transfer_amount) || 0,
|
||||||
|
}
|
||||||
|
|
||||||
hisMeny: user.history_transfer_amount,
|
// 处理账户列表数据
|
||||||
}
|
tableData.value = accounts.map((item: Account): TableDataRow => ({
|
||||||
|
tradeAccount: item.account_id,
|
||||||
// 这里替换成你的真实账户列表接口
|
accountName: item.account_name,
|
||||||
tableData.value = accounts.map((item: any) => ({
|
balance: Number(item.wallets_trade.amount) || 0,
|
||||||
tradeAccount: item.account_id,
|
leverage: Number(item.wallets_trade.lever) || 0,
|
||||||
accountName: item.account_name,
|
tradeVolume: Number(item.collect_data) || 0,
|
||||||
balance: item.wallets_trade.amount,
|
addTime: dayjs(item.created_at).format('YYYY-MM-DD HH:mm:ss'),
|
||||||
leverage: item.wallets_trade.lever,
|
}))
|
||||||
tradeVolume: item.collect_data,
|
|
||||||
addTime: dayjs(item.created_at).format('YYYY-MM-DD HH:mm:ss'),
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('加载详情失败:', err);
|
||||||
ElMessage.error('加载详情失败')
|
ElMessage.error('加载详情失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|
@ -203,8 +219,9 @@ watch(
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 账户操作
|
// 账户操作
|
||||||
const handleAccount = (row: any) => {
|
const handleAccount = (row: TableDataRow) => {
|
||||||
ElMessage.success('操作账户:' + row.tradeAccount)
|
ElMessage.success('操作账户:' + row.tradeAccount)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,12 @@
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">手机号</span>
|
<span class="label">手机号</span>
|
||||||
<span class="value">{{ userInfo.mobile || '未绑定' }}</span>
|
<span class="value">{{ userInfo.mobile || '未绑定' }}</span>
|
||||||
<el-button link type="primary" @click="showEditDialog('mobile')">
|
<el-button v-if="!userInfo.mobile" link type="primary" @click="showEditDialog('mobile')">
|
||||||
{{ userInfo.mobile ? '重新绑定' : '绑定' }}
|
绑定
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<template v-else>
|
||||||
|
<el-tag type="success" size="small">已绑定</el-tag>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
|
|
@ -63,56 +66,24 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 编辑弹窗 -->
|
<!-- 编辑弹窗 -->
|
||||||
<Dialog
|
<Dialog :visible="dialogVisible" :title="dialogTitle" width="500px" @confirm="handleEditConfirm"
|
||||||
:visible="dialogVisible"
|
@cancel="handleEditDialogReset" @close="handleEditDialogReset">
|
||||||
:title="dialogTitle"
|
<el-form ref="editFormRef" :model="editForm" :rules="getEditRules()" label-width="80px" label-position="top">
|
||||||
width="500px"
|
|
||||||
@confirm="handleEditConfirm"
|
|
||||||
@cancel="handleEditDialogReset"
|
|
||||||
@close="handleEditDialogReset"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="editFormRef"
|
|
||||||
:model="editForm"
|
|
||||||
:rules="getEditRules()"
|
|
||||||
label-width="80px"
|
|
||||||
label-position="top"
|
|
||||||
>
|
|
||||||
<el-form-item :label="editForm.label" prop="value">
|
<el-form-item :label="editForm.label" prop="value">
|
||||||
<el-input
|
<el-input v-if="editField !== 'language'" v-model="editForm.value" :placeholder="`请输入${editForm.label}`"
|
||||||
v-if="editField !== 'language'"
|
size="large" />
|
||||||
v-model="editForm.value"
|
|
||||||
:placeholder="`请输入${editForm.label}`"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<UserCaptcha
|
<UserCaptcha v-if="['email', 'mobile'].includes(editField)" v-model="editForm.code"
|
||||||
v-if="['email', 'mobile'].includes(editField)"
|
@sendCode="handleSendCaptcha" :show-send-button="showSendButton" :send-state="sendState" />
|
||||||
v-model="editForm.code"
|
|
||||||
@sendCode="handleSendCaptcha"
|
|
||||||
:show-send-button="showSendButton"
|
|
||||||
:send-state="sendState"
|
|
||||||
/>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- 密码修改弹窗 → 验证码模式 -->
|
<!-- 密码修改弹窗 → 验证码模式 -->
|
||||||
<Dialog
|
<Dialog :visible="passwordDialogVisible" title="修改密码" width="500px" @confirm="handlePasswordConfirm"
|
||||||
:visible="passwordDialogVisible"
|
@cancel="handlePasswordDialogReset" @close="handlePasswordDialogReset">
|
||||||
title="修改密码"
|
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordFormRules" label-width="100px"
|
||||||
width="500px"
|
label-position="top">
|
||||||
@confirm="handlePasswordConfirm"
|
|
||||||
@cancel="handlePasswordDialogReset"
|
|
||||||
@close="handlePasswordDialogReset"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="passwordFormRef"
|
|
||||||
:model="passwordForm"
|
|
||||||
:rules="passwordFormRules"
|
|
||||||
label-width="100px"
|
|
||||||
label-position="top"
|
|
||||||
>
|
|
||||||
<!-- 选择验证方式 -->
|
<!-- 选择验证方式 -->
|
||||||
<el-form-item label="验证方式" prop="verifyType">
|
<el-form-item label="验证方式" prop="verifyType">
|
||||||
<el-radio-group v-model="passwordForm.verifyType" @change="onVerifyTypeChange">
|
<el-radio-group v-model="passwordForm.verifyType" @change="onVerifyTypeChange">
|
||||||
|
|
@ -128,35 +99,20 @@
|
||||||
</span>
|
</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 验证码 -->
|
<!-- 验证码 -->
|
||||||
<UserCaptcha
|
<UserCaptcha prop="code" v-model="passwordForm.code" @sendCode="handleSendPwdCaptcha"
|
||||||
prop="code"
|
:show-send-button="showPwdSendButton" :send-state="pwdSendState" />
|
||||||
v-model="passwordForm.code"
|
|
||||||
@sendCode="handleSendPwdCaptcha"
|
|
||||||
:show-send-button="showPwdSendButton"
|
|
||||||
:send-state="pwdSendState"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 新密码1 -->
|
<!-- 新密码1 -->
|
||||||
<el-form-item label="新密码" prop="login_password1">
|
<el-form-item label="新密码" prop="login_password1">
|
||||||
<el-input
|
<el-input v-model="passwordForm.login_password1" type="password" show-password placeholder="6-12位,含大小写+特殊字符"
|
||||||
v-model="passwordForm.login_password1"
|
size="large" />
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="6-12位,含大小写+特殊字符"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 新密码2 -->
|
<!-- 新密码2 -->
|
||||||
<el-form-item label="确认密码" prop="login_password2">
|
<el-form-item label="确认密码" prop="login_password2">
|
||||||
<el-input
|
<el-input v-model="passwordForm.login_password2" type="password" show-password placeholder="请再次输入新密码"
|
||||||
v-model="passwordForm.login_password2"
|
size="large" />
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="请再次输入新密码"
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
@ -164,7 +120,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||||
import { UserFilled } from '@element-plus/icons-vue';
|
import { UserFilled } from '@element-plus/icons-vue';
|
||||||
import Dialog from '@/components/common/Dialog.vue';
|
import Dialog from '@/components/common/Dialog.vue';
|
||||||
|
|
@ -174,6 +130,18 @@ import { useLanguageStore } from '@/stores/modules/language';
|
||||||
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha';
|
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha';
|
||||||
import { codeRules } from '@/views/user/config/rules';
|
import { codeRules } from '@/views/user/config/rules';
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
import type {
|
||||||
|
UserInfo,
|
||||||
|
EditForm,
|
||||||
|
PasswordForm,
|
||||||
|
EditLangTypeParams,
|
||||||
|
BindMobileParams,
|
||||||
|
BindEmailParams,
|
||||||
|
EditUsernameParams,
|
||||||
|
UpdatePwdByCaptchaParams
|
||||||
|
} from '@/api/types/profile';
|
||||||
|
|
||||||
// 接口
|
// 接口
|
||||||
import {
|
import {
|
||||||
getUserInfoApi,
|
getUserInfoApi,
|
||||||
|
|
@ -181,10 +149,10 @@ import {
|
||||||
bindMobileApi,
|
bindMobileApi,
|
||||||
bindEmailApi,
|
bindEmailApi,
|
||||||
editUsernameApi,
|
editUsernameApi,
|
||||||
updatePwdByCaptchaApi // 统一修改密码接口
|
updatePwdByCaptchaApi
|
||||||
} from '@/api/modules/profile/personalCenter';
|
} from '@/api/modules/profile/personalCenter';
|
||||||
|
|
||||||
const userInfo = reactive({
|
const userInfo = reactive<UserInfo>({
|
||||||
id: 0,
|
id: 0,
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
|
@ -198,20 +166,20 @@ const dialogVisible = ref(false);
|
||||||
const passwordDialogVisible = ref(false);
|
const passwordDialogVisible = ref(false);
|
||||||
const editFormRef = ref<FormInstance>();
|
const editFormRef = ref<FormInstance>();
|
||||||
const passwordFormRef = ref<FormInstance>();
|
const passwordFormRef = ref<FormInstance>();
|
||||||
const editField = ref('');
|
const editField = ref<string>('');
|
||||||
const dialogTitle = ref('');
|
const dialogTitle = ref('');
|
||||||
|
|
||||||
const editForm = reactive({ label: '', value: '', code: '' });
|
const editForm = reactive<EditForm>({ label: '', value: '', code: '' });
|
||||||
|
|
||||||
// 密码表单 → 完全按你后端字段命名
|
// 密码表单
|
||||||
const passwordForm = reactive({
|
const passwordForm = reactive<PasswordForm>({
|
||||||
verifyType: 'mobile' as 'mobile' | 'email',
|
verifyType: 'mobile',
|
||||||
code: '',
|
code: '',
|
||||||
login_password1: '',
|
login_password1: '',
|
||||||
login_password2: ''
|
login_password2: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// 验证码
|
// 验证码状态
|
||||||
const showSendButton = ref(true);
|
const showSendButton = ref(true);
|
||||||
const sendState = ref({ isCounting: false, countdown: 0 });
|
const sendState = ref({ isCounting: false, countdown: 0 });
|
||||||
const phoneCaptchaState = initCaptchaState('phone');
|
const phoneCaptchaState = initCaptchaState('phone');
|
||||||
|
|
@ -262,29 +230,40 @@ const passwordFormRules = ref<FormRules>({
|
||||||
login_password1: [
|
login_password1: [
|
||||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
{ min: 6, max: 12, message: '长度 6-12 位', trigger: 'blur' },
|
{ min: 6, max: 12, message: '长度 6-12 位', trigger: 'blur' },
|
||||||
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{6,12}$/,
|
{
|
||||||
message: '必须包含大小写+特殊字符', trigger: 'blur' }
|
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{6,12}$/,
|
||||||
|
message: '必须包含大小写+特殊字符', trigger: 'blur'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
login_password2: [
|
login_password2: [
|
||||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value !== passwordForm.login_password1) callback(new Error('两次密码不一致'));
|
if (value !== passwordForm.login_password1) {
|
||||||
callback();
|
callback(new Error('两次密码不一致'));
|
||||||
}, trigger: 'blur'
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
const res = await getUserInfoApi();
|
try {
|
||||||
Object.assign(userInfo, res);
|
const res = await getUserInfoApi();
|
||||||
languageStore.changeLang(res.lang_type === 1 ? 'zh-CN' : 'en-US');
|
Object.assign(userInfo, res);
|
||||||
|
languageStore.changeLang(res.lang_type === 1 ? 'zh-CN' : 'en-US');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取用户信息失败');
|
||||||
|
console.error('获取用户信息失败:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开编辑弹窗
|
// 打开编辑弹窗
|
||||||
const showEditDialog = (field: string) => {
|
const showEditDialog = (field: 'email' | 'mobile' | 'username') => {
|
||||||
editField.value = field;
|
editField.value = field;
|
||||||
editForm.code = '';
|
editForm.code = '';
|
||||||
switch (field) {
|
switch (field) {
|
||||||
|
|
@ -312,6 +291,10 @@ const showEditDialog = (field: string) => {
|
||||||
const handleSendCaptcha = async () => {
|
const handleSendCaptcha = async () => {
|
||||||
const type = editField.value === 'email' ? 'email' : 'phone';
|
const type = editField.value === 'email' ? 'email' : 'phone';
|
||||||
const receiver = editForm.value?.trim();
|
const receiver = editForm.value?.trim();
|
||||||
|
if (!receiver) {
|
||||||
|
ElMessage.warning('请输入接收验证码的账号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const currentState = type === 'email' ? emailCaptchaState : phoneCaptchaState;
|
const currentState = type === 'email' ? emailCaptchaState : phoneCaptchaState;
|
||||||
await sendCaptcha(type, receiver, currentState, () => {
|
await sendCaptcha(type, receiver, currentState, () => {
|
||||||
return userStore.sendCode({
|
return userStore.sendCode({
|
||||||
|
|
@ -325,43 +308,53 @@ const handleSendCaptcha = async () => {
|
||||||
|
|
||||||
const updateSendState = () => {
|
const updateSendState = () => {
|
||||||
const t = editField.value === 'email' ? emailCaptchaState : phoneCaptchaState;
|
const t = editField.value === 'email' ? emailCaptchaState : phoneCaptchaState;
|
||||||
sendState.value = { isCounting: t.isCounting, countdown: t.countdown };
|
sendState.value = { isCounting: t.isCounting.value, countdown: t.countdown.value };
|
||||||
};
|
};
|
||||||
|
|
||||||
// 语言切换
|
// 语言切换
|
||||||
const handleLanguageChange = async (val: number) => {
|
const handleLanguageChange = async (val: number) => {
|
||||||
await editLangTypeApi(val);
|
try {
|
||||||
userInfo.lang_type = val;
|
await editLangTypeApi({ lang_type: val });
|
||||||
languageStore.changeLang(val === 1 ? 'zh-CN' : 'en-US');
|
userInfo.lang_type = val;
|
||||||
ElMessage.success('语言切换成功');
|
languageStore.changeLang(val === 1 ? 'zh-CN' : 'en-US');
|
||||||
|
ElMessage.success('语言切换成功');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('语言切换失败');
|
||||||
|
console.error('语言切换失败:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交修改
|
// 提交修改
|
||||||
const handleEditConfirm = async () => {
|
const handleEditConfirm = async () => {
|
||||||
if (!editFormRef.value) return;
|
if (!editFormRef.value) return;
|
||||||
await editFormRef.value.validate();
|
|
||||||
try {
|
try {
|
||||||
|
await editFormRef.value.validate();
|
||||||
|
|
||||||
if (editField.value === 'username') {
|
if (editField.value === 'username') {
|
||||||
await editUsernameApi({ username: editForm.value, user_id: userInfo.id });
|
const params: EditUsernameParams = { username: editForm.value, user_id: userInfo.id };
|
||||||
|
await editUsernameApi(params);
|
||||||
userInfo.username = editForm.value;
|
userInfo.username = editForm.value;
|
||||||
}
|
}
|
||||||
if (editField.value === 'mobile') {
|
if (editField.value === 'mobile') {
|
||||||
await bindMobileApi({ mobile: editForm.value, code: editForm.code });
|
const params: BindMobileParams = { mobile: editForm.value, code: editForm.code };
|
||||||
|
await bindMobileApi(params);
|
||||||
userInfo.mobile = editForm.value;
|
userInfo.mobile = editForm.value;
|
||||||
}
|
}
|
||||||
if (editField.value === 'email') {
|
if (editField.value === 'email') {
|
||||||
await bindEmailApi({ email: editForm.value, code: editForm.code });
|
const params: BindEmailParams = { email: editForm.value, code: editForm.code };
|
||||||
|
await bindEmailApi(params);
|
||||||
userInfo.email = editForm.value;
|
userInfo.email = editForm.value;
|
||||||
}
|
}
|
||||||
ElMessage.success('操作成功');
|
ElMessage.success('操作成功');
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
editFormRef.value.resetFields();
|
editFormRef.value.resetFields();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error('操作失败');
|
// 表单验证失败或API调用失败时,不显示错误消息,因为ElMessage已经在其他地方处理
|
||||||
|
console.error('操作失败:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 密码修改逻辑(完全按你接口格式) ====================
|
// ==================== 密码修改逻辑 ====================
|
||||||
const showPasswordDialog = () => {
|
const showPasswordDialog = () => {
|
||||||
if (!userInfo.mobile && !userInfo.email) {
|
if (!userInfo.mobile && !userInfo.email) {
|
||||||
ElMessage.warning('请先绑定手机或邮箱');
|
ElMessage.warning('请先绑定手机或邮箱');
|
||||||
|
|
@ -373,17 +366,31 @@ const showPasswordDialog = () => {
|
||||||
passwordForm.login_password2 = '';
|
passwordForm.login_password2 = '';
|
||||||
passwordDialogVisible.value = true;
|
passwordDialogVisible.value = true;
|
||||||
updatePwdSendState();
|
updatePwdSendState();
|
||||||
|
|
||||||
|
// 修复:打开弹窗后清除验证状态,避免一进入就触发验证
|
||||||
|
nextTick(() => {
|
||||||
|
passwordFormRef.value?.clearValidate();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onVerifyTypeChange = () => {
|
const onVerifyTypeChange = () => {
|
||||||
passwordForm.code = '';
|
passwordForm.code = '';
|
||||||
updatePwdSendState();
|
updatePwdSendState();
|
||||||
|
|
||||||
|
// 切换验证方式时清除验证状态
|
||||||
|
nextTick(() => {
|
||||||
|
passwordFormRef.value?.clearValidate(['code']);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送修改密码验证码
|
// 发送修改密码验证码
|
||||||
const handleSendPwdCaptcha = async () => {
|
const handleSendPwdCaptcha = async () => {
|
||||||
const type = passwordForm.verifyType === 'email' ? 'email' : 'phone';
|
const type = passwordForm.verifyType === 'email' ? 'email' : 'phone';
|
||||||
const receiver = passwordForm.verifyType === 'email' ? userInfo.email : userInfo.mobile;
|
const receiver = passwordForm.verifyType === 'email' ? userInfo.email : userInfo.mobile;
|
||||||
|
if (!receiver) {
|
||||||
|
ElMessage.warning('请先绑定验证账号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const currentState = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
|
const currentState = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
|
||||||
|
|
||||||
await sendCaptcha(type, receiver, currentState, () => {
|
await sendCaptcha(type, receiver, currentState, () => {
|
||||||
|
|
@ -398,16 +405,16 @@ const handleSendPwdCaptcha = async () => {
|
||||||
|
|
||||||
const updatePwdSendState = () => {
|
const updatePwdSendState = () => {
|
||||||
const t = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
|
const t = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
|
||||||
pwdSendState.value = { isCounting: t.isCounting, countdown: t.countdown };
|
pwdSendState.value = { isCounting: t.isCounting.value, countdown: t.countdown.value };
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交修改密码 → 100% 按你给的参数格式
|
// 提交修改密码
|
||||||
const handlePasswordConfirm = async () => {
|
const handlePasswordConfirm = async () => {
|
||||||
if (!passwordFormRef.value) return;
|
if (!passwordFormRef.value) return;
|
||||||
await passwordFormRef.value.validate();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = {
|
await passwordFormRef.value.validate();
|
||||||
|
|
||||||
|
const params: UpdatePwdByCaptchaParams = {
|
||||||
reg_type: passwordForm.verifyType === 'email' ? 1 : 2,
|
reg_type: passwordForm.verifyType === 'email' ? 1 : 2,
|
||||||
email: passwordForm.verifyType === 'email' ? userInfo.email : undefined,
|
email: passwordForm.verifyType === 'email' ? userInfo.email : undefined,
|
||||||
mobile: passwordForm.verifyType === 'mobile' ? userInfo.mobile : undefined,
|
mobile: passwordForm.verifyType === 'mobile' ? userInfo.mobile : undefined,
|
||||||
|
|
@ -421,7 +428,8 @@ const handlePasswordConfirm = async () => {
|
||||||
passwordDialogVisible.value = false;
|
passwordDialogVisible.value = false;
|
||||||
passwordFormRef.value.resetFields();
|
passwordFormRef.value.resetFields();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error('密码修改失败');
|
// 表单验证失败或API调用失败时,不显示错误消息,因为ElMessage已经在其他地方处理
|
||||||
|
console.error('密码修改失败:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -450,8 +458,10 @@ onUnmounted(() => {
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.personal-center {
|
.personal-center {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -478,6 +488,7 @@ onUnmounted(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
.avatar-wrapper {
|
.avatar-wrapper {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
|
|
@ -487,24 +498,29 @@ onUnmounted(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.avatar-image {
|
.avatar-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-icon {
|
.avatar-icon {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-text {
|
.info-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|
||||||
.username-row {
|
.username-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.username-text {
|
.username-text {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -512,6 +528,7 @@ onUnmounted(() => {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-time {
|
.login-time {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
|
|
@ -526,21 +543,25 @@ onUnmounted(() => {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
.info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
border-bottom: 1px solid #f2f3f5;
|
border-bottom: 1px solid #f2f3f5;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
min-width: 70px;
|
min-width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue