入金货币,二维码生成

This commit is contained in:
Songsl 2026-05-07 15:58:01 +08:00
parent 47c2ae7d42
commit 0e195a5434
9 changed files with 401 additions and 93 deletions

View File

@ -28,6 +28,7 @@
"element-plus": "^2.13.2", "element-plus": "^2.13.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"qrcode": "^1.5.4",
"vue": "^3.5.29", "vue": "^3.5.29",
"vue-router": "^5.0.3", "vue-router": "^5.0.3",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"

View File

@ -0,0 +1,18 @@
import { request } from '@/api'
// 获取入金货币列表
export const getCurrencyList = () => {
return request.get('/currency/getCurrencyList')
}
// 创建入金货币
export const createCurrency = (params: any) => {
return request.post('/currency/createCurrency', { ...params })
}
// 编辑入金货币
export const editCurrency = (params: any) => {
return request.post('/currency/editCurrency', { ...params })
}
// 删除入金货币
export const delCurrency = (params: any) => {
return request.post('/currency/delCurrency', { ...params })
}

View File

@ -1,14 +1,20 @@
<!-- src/components/CommonTableHeader.vue --> <!-- src/components/CommonTableHeader.vue -->
<template> <template>
<div class="header"> <div class="header">
<h4>{{ title }}</h4> <h4>{{ title }}</h4>
<div class="header-actions"> <div class="header-actions">
<el-button v-for="(btn, index) in buttons" :key="btn.key || index" :type="btn.type || 'default'" <el-button
:icon="btn.icon" @click="handleButtonClick(btn)" :disabled="btn.disabled"> v-for="(btn, index) in buttons"
{{ btn.label }} :key="btn.key || index"
</el-button> :type="btn.type || 'default'"
</div> :icon="btn.icon"
@click="handleButtonClick(btn)"
:disabled="btn.disabled"
>
{{ btn.label }}
</el-button>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -17,29 +23,47 @@ import type { Component } from 'vue'
// //
interface HeaderButton { interface HeaderButton {
label: string label: string
type?: ButtonProps['type'] type?: ButtonProps['type']
icon?: Component icon?: Component
disabled?: boolean disabled?: boolean
key?: string | number // key?: string | number //
} }
// //
interface Props { interface Props {
title: string title: string
buttons: HeaderButton[] buttons: HeaderButton[]
} }
const props = defineProps<Props>() const props = defineProps<Props>()
// //
const emit = defineEmits<{ const emit = defineEmits<{
// button-click // button-click
'button-click': [button: HeaderButton] 'button-click': [button: HeaderButton]
}>() }>()
// //
const handleButtonClick = (button: HeaderButton) => { const handleButtonClick = (button: HeaderButton) => {
// //
emit('button-click', button) emit('button-click', button)
} }
</script> </script>
<style scoped lang="scss">
.header {
display: flex;
align-items: center;
margin-bottom: 10px;
h4 {
margin: 0 40px 0 0;
font-size: 24px;
font-weight: 500;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
}
</style>

View File

@ -1,7 +1,162 @@
<script setup lang="ts"></script>
<template> <template>
<h1>入金货币</h1> <!-- 搜索 -->
<FundDetailHeader title="入金货币" :buttons="buttonsOptions" @button-click="handleButtonClick">
</FundDetailHeader>
<!-- 表格 -->
<FundDetailTable
:table-data="tableData"
:columns="tableColumnsOptions"
@row-click="handleRowClick"
row-key="id"
/>
<FundDetailDialog
:visible="visible"
:title="dialogTitle"
@close="visible = false"
@cancel="visible = false"
@confirm="handleConfirm"
>
<FundDetailForm
:formData="formData"
:formItems="formItems"
:formRules="formRules"
:optionsMap="optionsMap"
></FundDetailForm>
</FundDetailDialog>
</template> </template>
<script setup lang="ts">
import FundDetailHeader from '@/components/common/header.vue'
import FundDetailTable from '@/components/common/Table.vue'
import FundDetailDialog from '@/components/common/Dialog.vue'
import FundDetailForm from '@/components/common/Form.vue'
import type { ButtonProps } from 'element-plus'
import type { Component } from 'vue'
import { buttons, tableColumns, form, rules } from './options'
import {
getCurrencyList,
createCurrency,
delCurrency,
editCurrency,
} from '@/api/modules/capital/currency.ts'
//
interface HeaderButton {
label: string
type?: ButtonProps['type']
icon?: Component
disabled?: boolean
key?: string | number //
}
const buttonsOptions = ref(buttons)
const formItems = ref(form)
const tableData = ref([])
const tableColumnsOptions = ref(tableColumns)
const dialogTitle = ref('')
const formRules = ref(rules)
const formData = ref({
name: '',
symbol: '',
rate: null,
type: 1,
status: 1,
})
const optionsMap = {
type: [
{ label: '法币', value: 1 },
{ label: '数字货币', value: 2 },
],
status: [
{ label: '正常', value: 1 },
{ label: '禁用', value: 2 },
],
}
let rowData: any = {}
const visible = ref<boolean>(false)
const getTableList = async () => {
const data = await getCurrencyList()
tableData.value = (data || []).map((item: any) => {
return {
...item,
typeStr: ['', '法币', '数字货币'][item.type],
statusStr: ['', '正常', '禁用'][item.status],
}
})
console.log('tableData.value', tableData.value)
}
const handleRowClick = (row: any) => {
console.log(row)
rowData = row
}
const resetForm = () => {
formData.value = {
name: '',
symbol: '',
rate: null,
type: 1,
status: 1,
}
}
const handleButtonClick = (button: HeaderButton) => {
console.log(button)
switch (button.key) {
case 'add':
handleAdd()
break
case 'edit':
if (!rowData.id) return ElMessage.warning('请选择数据')
handleEdit()
break
case 'delete':
ElMessageBox.confirm('你确定要删除此数据').then(async () => {
await delCurrency({ id: rowData.id })
ElMessage.success('删除成功')
await getTableList()
})
break
}
}
const handleAdd = () => {
dialogTitle.value = '添加'
visible.value = true
}
const handleEdit = () => {
dialogTitle.value = '编辑'
visible.value = true
formData.value = {
name: rowData.name,
symbol: rowData.symbol,
rate: rowData.rate,
type: rowData.type,
status: rowData.status,
}
}
const handleConfirm = async () => {
if (dialogTitle.value === '添加') {
await createCurrency(formData.value)
} else {
await editCurrency({ ...formData.value, id: rowData.id })
}
visible.value = false
resetForm()
await getTableList()
}
onMounted(() => {
getTableList()
})
</script>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -0,0 +1,32 @@
/**
* @description:
*/
export const buttons = [
{ label: '添加', type: 'primary', key: 'add' },
{ label: '编辑', type: 'primary', key: 'edit' },
{ label: '删除', type: 'danger', key: 'delete' },
]
export const tableColumns = [
{ prop: 'name', label: '币名' },
{ prop: 'symbol', label: '货币符号' },
{ prop: 'rate', label: '汇率' },
{ prop: 'typeStr', label: '货币类型' },
{ prop: 'statusStr', label: '状态' },
{ prop: 'created_at', label: '创建时间' },
]
export const form = [
{ prop: 'name', label: '币名', type: 'input', placeholder: '请输入币名' },
{ prop: 'symbol', label: '货币符号', type: 'input', placeholder: '请输入货币符号' },
{ prop: 'rate', label: '汇率', type: 'number', placeholder: '请输入汇率' },
{ prop: 'type', label: '货币类型', type: 'radio', placeholder: '请输入货币类型' },
{ prop: 'status', label: '状态', type: 'radio', placeholder: '请输入状态' },
]
export const rules = {
name: [{ required: true, message: '请输入币名', trigger: ['change'] }],
symbol: [{ required: true, message: '请输入货币符号', trigger: ['change'] }],
rate: [{ required: true, message: '请输入汇率', trigger: ['change'] }],
type: [{ required: true, message: '请输入货币类型', trigger: ['change'] }],
status: [{ required: true, message: '请输入状态', trigger: ['change'] }],
}

View File

@ -154,7 +154,7 @@
</div> </div>
</div> </div>
</el-card> </el-card>
<el-card class="qr-code"> <el-card class="qr-code" v-if="userInfo.type * 1 === 2 && userInfo.role_id * 1 === 5">
<h3> <h3>
推广 推广
<span>统计起始时间:2026-3-20</span> <span>统计起始时间:2026-3-20</span>
@ -172,10 +172,7 @@
<div class="item"> <div class="item">
<span>推广二维码</span> <span>推广二维码</span>
<div class="text"> <div class="text">
<img :src="qrCode" alt="推广二维码" /> <canvas ref="qrCanvas"></canvas>
<el-icon @click="handleCopyQrCode">
<DocumentCopy />
</el-icon>
</div> </div>
</div> </div>
</div> </div>
@ -215,8 +212,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { CircleCheckFilled, EditPen, Setting, DocumentCopy } from '@element-plus/icons-vue' import { CircleCheckFilled, EditPen, Setting, DocumentCopy } from '@element-plus/icons-vue'
import qrCodeImg from '@/assets/images/qrCode.png'
// //
import AgentDialog from '@/components/common/Dialog.vue' import AgentDialog from '@/components/common/Dialog.vue'
import AgentForm from '@/components/common/Form.vue' import AgentForm from '@/components/common/Form.vue'
@ -241,6 +236,8 @@ import { useUserStore } from '@/stores/modules/user.ts'
const userStore = useUserStore() const userStore = useUserStore()
import QRCode from 'qrcode'
/** /**
* @description: dialog数据 * @description: dialog数据
*/ */
@ -303,8 +300,8 @@ const inputRef = ref<HTMLInputElement>()
/* /*
* @description: 复制推广二维码和推广链接数据 * @description: 复制推广二维码和推广链接数据
*/ */
const copyText = ref('3123123213123123123') const copyText = ref(userInfo.invite_url)
const qrCode = ref(qrCodeImg) const qrCanvas = ref()
/** /**
* @description: 定义公共请求数据方法 * @description: 定义公共请求数据方法
@ -322,6 +319,16 @@ const getLeverOptions = async () => {
} }
} }
const generateBasic = async () => {
const userInfo = getStorage('userInfo')
if (!userInfo?.invite_url || !qrCanvas.value) return
await QRCode.toCanvas(qrCanvas.value, userInfo.invite_url, {
width: 200,
margin: 2,
})
}
/** /**
* @description: 页面加载完成需要请求的数据 * @description: 页面加载完成需要请求的数据
*/ */
@ -329,6 +336,7 @@ onMounted(() => {
getLeverOptions() getLeverOptions()
getAccoundList() getAccoundList()
getWalletCapitaInfo() getWalletCapitaInfo()
generateBasic()
}) })
/** /**
@ -424,36 +432,52 @@ const handleSetting = (id: string) => {
* @description: 复制推广二维码和推广链接方法 * @description: 复制推广二维码和推广链接方法
*/ */
const handleCopyUrl = async () => { const handleCopyUrl = async () => {
try { const success = await copyToClipboard(copyText.value)
await navigator.clipboard.writeText(copyText.value) if (success) {
ElMessage.success('复制成功!') alert('复制成功')
} catch (err) { } else {
console.error('复制失败:', err) alert('复制失败,请手动复制')
ElMessage.error('复制失败,请重试')
} }
} }
const handleCopyQrCode = async () => {
try {
// 1. Blob
const response = await fetch(qrCode.value)
const blob = await response.blob()
// 2. png/jpeg async function copyToClipboard(text: string) {
await navigator.clipboard.write([ // 1: 使 Clipboard API
new ClipboardItem({ if (navigator.clipboard && navigator.clipboard.writeText) {
[blob.type]: blob, try {
}), await navigator.clipboard.writeText(text)
]) return true
ElMessage.success('图片复制成功!') } catch (err) {
console.warn('Clipboard API 失败,尝试降级方案', err)
}
}
// 2: document.execCommand HTTP
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.left = '-9999px'
textarea.style.top = '0'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
try {
const success = document.execCommand('copy')
document.body.removeChild(textarea)
return success
} catch (err) { } catch (err) {
console.error('复制图片失败:', err) document.body.removeChild(textarea)
ElMessage.error('图片复制失败,请检查图片格式或权限') console.error('复制失败:', err)
return false
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.agent-container { .agent-container {
width: 100%;
height: 100%;
overflow-y: auto;
.el-card { .el-card {
margin-bottom: 20px; margin-bottom: 20px;

View File

@ -68,7 +68,6 @@
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { Plus } from '@element-plus/icons-vue' import { Plus } from '@element-plus/icons-vue'
// //
import MenuHeader from '@/components/common/Header.vue'
import MenuTable from '@/components/common/Table.vue' import MenuTable from '@/components/common/Table.vue'
import MenuDialog from '@/components/common/Dialog.vue' import MenuDialog from '@/components/common/Dialog.vue'
import MenuForm from '@/components/common/Form.vue' import MenuForm from '@/components/common/Form.vue'

View File

@ -104,7 +104,6 @@ import type { FormInstance } from 'element-plus'
import { Plus } from '@element-plus/icons-vue' import { Plus } from '@element-plus/icons-vue'
// //
import RoleHeader from '@/components/common/Header.vue'
import RoleTable from '@/components/common/Table.vue' import RoleTable from '@/components/common/Table.vue'
import RoleDialog from '@/components/common/Dialog.vue' import RoleDialog from '@/components/common/Dialog.vue'
import RoleForm from '@/components/common/Form.vue' import RoleForm from '@/components/common/Form.vue'

View File

@ -5,41 +5,93 @@
<UserTabs v-model="activeTab" :tab-list="tabList" /> <UserTabs v-model="activeTab" :tab-list="tabList" />
<el-form class="user-form" ref="registerFormRef" :model="registerForm" :rules="registerRules"> <el-form class="user-form" ref="registerFormRef" :model="registerForm" :rules="registerRules">
<Transition name="form-switch" mode="out-in"> <Transition name="form-switch" mode="out-in">
<el-form-item class="captcha-item" v-if="activeTab === 'email'" key="form-email-item" prop="email"> <el-form-item
class="captcha-item"
v-if="activeTab === 'email'"
key="form-email-item"
prop="email"
>
<el-input v-model="registerForm.email" placeholder="请输入邮箱" clearable size="large" /> <el-input v-model="registerForm.email" placeholder="请输入邮箱" clearable size="large" />
<el-button type="primary" class="captcha-btn" @click="handleSendCode" :disabled="emailIsCounting">{{ <el-button
emailIsCounting ? `${emailCountdown}s后重新获取` : '发送验证码' }}</el-button> type="primary"
class="captcha-btn"
@click="handleSendCode"
:disabled="emailIsCounting"
>{{ emailIsCounting ? `${emailCountdown}s后重新获取` : '发送验证码' }}</el-button
>
</el-form-item> </el-form-item>
<el-form-item class="phone-item captcha-item" prop="phone" v-else-if="activeTab === 'phone'" <el-form-item
key="form-phone-item"> class="phone-item captcha-item"
<el-select v-model="registerForm.countryCode" :teleported="false" value-key="code" placeholder="国家" prop="phone"
size="large"> v-else-if="activeTab === 'phone'"
<el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code"> key="form-phone-item"
>
<el-select
v-model="registerForm.countryCode"
:teleported="false"
value-key="code"
placeholder="国家"
size="large"
>
<el-option
v-for="item in countryOptions"
:key="item.code"
:label="item.code"
:value="item.code"
>
<div class="country-option"> <div class="country-option">
<span class="country-name">{{ item.name }}</span> <span class="country-name">{{ item.name }}</span>
<span class="country-code">{{ item.code }}</span> <span class="country-code">{{ item.code }}</span>
</div> </div>
</el-option> </el-option>
</el-select> </el-select>
<el-input v-model="registerForm.phone" placeholder="请输入手机号" clearable size="large" /> <el-input
<el-button type="primary" class="captcha-btn" @click="handleSendCode" :disabled="phoneIsCounting">{{ v-model="registerForm.phone"
phoneIsCounting ? `${phoneCountdown}s后重新获取` : '发送验证码' }}</el-button> placeholder="请输入手机号"
clearable
size="large"
/>
<el-button
type="primary"
class="captcha-btn"
@click="handleSendCode"
:disabled="phoneIsCounting"
>{{ phoneIsCounting ? `${phoneCountdown}s后重新获取` : '发送验证码' }}</el-button
>
</el-form-item> </el-form-item>
</Transition> </Transition>
<UserCaptcha v-model="registerForm.code" /> <UserCaptcha v-model="registerForm.code" />
<el-form-item label="设置密码(6-12位密码包含特殊符号以及大小写)" label-position="top" prop="password"> <el-form-item
<el-input placeholder="请输入密码" clearable show-password size="large" type="password" label="设置密码(6-12位密码包含特殊符号以及大小写)"
v-model="registerForm.password" /> label-position="top"
prop="password"
>
<el-input
placeholder="请输入密码"
clearable
show-password
size="large"
type="password"
v-model="registerForm.password"
/>
</el-form-item> </el-form-item>
<el-form-item label="邀请链接/邀请码(选填)" label-position="top" prop="inviteCode"> <el-form-item label="邀请链接/邀请码(选填)" label-position="top" prop="inviteCode">
<el-input placeholder="请输入邀请链接/邀请码" clearable size="large" type="text" v-model="registerForm.inviteCode" /> <el-input
placeholder="请输入邀请链接/邀请码"
clearable
size="large"
type="text"
v-model="registerForm.inviteCode"
/>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-checkbox label="同意注册协议">我已阅读并同意 隐私条例 退款政策 AMLCTF政策<br> 执行政策 网站条款和条件</el-checkbox> <el-checkbox label="同意注册协议"
>我已阅读并同意 隐私条例 退款政策 AMLCTF政策<br />
执行政策 网站条款和条件</el-checkbox
>
</el-form-item> </el-form-item>
<el-form-item class="form-submit"> <el-form-item class="form-submit">
<el-button type="primary" class="form-btn" @click="handleRegister">注册</el-button> <el-button type="primary" class="form-btn" @click="handleRegister">注册</el-button>
@ -66,16 +118,18 @@ import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCa
// //
import { countryOptions } from '@/views/user/config/mock' import { countryOptions } from '@/views/user/config/mock'
import { passwordRules, emailRules, phoneRules, codeRules } from './config/rules' import { passwordRules, emailRules, phoneRules, codeRules } from './config/rules'
import { useRoute } from 'vue-router'
//store //store
const userStore = useUserStore() const userStore = useUserStore()
const route = useRoute()
// UserText // UserText
const userText = ref({ const userText = ref({
backText: '', backText: '',
title: '注册', title: '注册',
text: '', text: '',
}); })
//tabs //tabs
const tabList = ref([ const tabList = ref([
@ -92,7 +146,7 @@ const registerForm = ref({
phone: '', phone: '',
password: '', password: '',
code: '', code: '',
inviteCode: '' inviteCode: '',
}) })
const registerRules = ref<FormRules>({ const registerRules = ref<FormRules>({
email: emailRules().email, email: emailRules().email,
@ -102,14 +156,13 @@ const registerRules = ref<FormRules>({
}) })
// //
const emailCaptchaState = initCaptchaState('email'); // const emailCaptchaState = initCaptchaState('email') //
const phoneCaptchaState = initCaptchaState('phone'); // const phoneCaptchaState = initCaptchaState('phone') //
// //
const { isCounting: emailIsCounting, countdown: emailCountdown } = emailCaptchaState const { isCounting: emailIsCounting, countdown: emailCountdown } = emailCaptchaState
// //
const { isCounting: phoneIsCounting, countdown: phoneCountdown } = phoneCaptchaState const { isCounting: phoneIsCounting, countdown: phoneCountdown } = phoneCaptchaState
// //
// //
@ -119,7 +172,7 @@ const sendCodeApi = async () => {
email: registerForm.value.email, email: registerForm.value.email,
mobile: registerForm.value.phone, mobile: registerForm.value.phone,
reg_type: activeTab.value === 'email' ? 1 : 2, reg_type: activeTab.value === 'email' ? 1 : 2,
type:'resetPass_code' type: 'reg_code',
} }
return userStore.sendCode(sendParams) return userStore.sendCode(sendParams)
} }
@ -134,26 +187,26 @@ const handleSendCode = async () => {
} }
// //
const type: string = activeTab.value === 'phone' ? 'phone' : 'email'; const type: string = activeTab.value === 'phone' ? 'phone' : 'email'
const receiver = type === 'phone' ? registerForm.value.phone : registerForm.value.email; const receiver = type === 'phone' ? registerForm.value.phone : registerForm.value.email
const currentCaptchaState = type === 'phone' ? phoneCaptchaState : emailCaptchaState; const currentCaptchaState = type === 'phone' ? phoneCaptchaState : emailCaptchaState
const sendFunc = type === 'phone' ? "sendPhoneCaptcha" : "sendEmailCaptcha"; const sendFunc = type === 'phone' ? 'sendPhoneCaptcha' : 'sendEmailCaptcha'
// //
await sendCaptcha( await sendCaptcha(
type, type,
receiver, receiver,
currentCaptchaState, // currentCaptchaState, //
() => sendCodeApi() // Promise () => sendCodeApi(), // Promise
); )
} }
// //
const handleRegister = async () => { const handleRegister = async () => {
if (!registerFormRef.value) return; if (!registerFormRef.value) return
try { try {
// //
await registerFormRef.value.validate(); await registerFormRef.value.validate()
// //
const registerParams = { const registerParams = {
@ -162,25 +215,28 @@ const handleRegister = async () => {
reg_type: activeTab.value === 'email' ? 1 : 2, reg_type: activeTab.value === 'email' ? 1 : 2,
invite_code: registerForm.value.inviteCode, invite_code: registerForm.value.inviteCode,
code: registerForm.value.code, code: registerForm.value.code,
password: registerForm.value.password password: registerForm.value.password,
}; }
// //
await userStore.register(registerParams); await userStore.register(registerParams)
} catch (error) { } catch (error) {
console.error('表单校验失败:', error); console.error('表单校验失败:', error)
} }
} }
// //
onUnmounted(() => { onUnmounted(() => {
clearCaptchaTimer(emailCaptchaState); clearCaptchaTimer(emailCaptchaState)
clearCaptchaTimer(phoneCaptchaState); clearCaptchaTimer(phoneCaptchaState)
}); })
onMounted(()=>{
registerForm.value.inviteCode = route.query.invite_code
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.user-form { .user-form {
.phone-item, .phone-item,
.captcha-item { .captcha-item {
:deep(.el-form-item__content) { :deep(.el-form-item__content) {