feat: show own withdrawal account details and enable avatar upload

This commit is contained in:
richjohnrambo
2026-10-16 00:19:11 +08:00
parent 07c654b6d4
commit ecf1a89313
8 changed files with 355 additions and 36 deletions
+111 -32
View File
@@ -2,11 +2,6 @@
<section
class="m-page m-page--white m-subpage"
:class="{ 'm-subpage--profile': formType === 'profile' }"
:style="
formType === 'profile'
? { backgroundImage: `url(${mobileAssets.profileBackground})` }
: undefined
"
>
<PageHeader :title="title" />
<AsyncState
@@ -15,35 +10,63 @@
:error="error"
@retry="loadProfile"
>
<div class="m-profile-edit">
<div class="m-avatar m-avatar--large"
><img
v-if="safeImage(me?.avatar_url)"
:src="safeImage(me.avatar_url)"
alt="用户头像" /><n-icon v-else :component="PersonCircleOutline" /><img
class="m-avatar-edit"
:src="mobileAssets.avatarEdit"
alt=""
/></div>
<div class="m-info-card m-card">
<div
><span>昵称</span><b>{{ me?.username }}</b></div
><div
><span>手机号码</span><b>{{ me?.phone }}</b></div
><div
><span>邀请码</span><b>{{ me?.invite_code }}</b></div
>
<router-link to="/app/profile/password"
><span>密码管理</span><n-icon :component="ChevronForwardOutline"
/></router-link>
<router-link to="/app/profile/trade-password"
><span>交易密码管理</span><n-icon :component="ChevronForwardOutline"
/></router-link>
<div class="m-profile-info">
<div class="m-profile-info-card m-card">
<label class="m-profile-info-row m-profile-avatar-row">
<n-icon :component="ImageOutline" />
<span>头像</span>
<span class="m-profile-avatar-preview">
<img v-if="safeImage(me?.avatar_url)" :src="safeImage(me.avatar_url)" alt="用户头像" />
<n-icon v-else :component="PersonCircleOutline" />
<small v-if="avatarUploading">上传中...</small>
</span>
<n-icon :component="ChevronForwardOutline" />
<input type="file" accept="image/jpeg,image/png,image/webp" :disabled="avatarUploading" @change="uploadAvatar" />
</label>
</div>
<p v-if="avatarError" class="m-form-error">{{ avatarError }}</p>
<div class="m-profile-info-card m-card">
<div class="m-profile-info-row">
<n-icon :component="PhonePortraitOutline" />
<span>我的手机号</span>
<b>{{ me?.phone }}</b>
</div>
<router-link class="m-profile-info-row" to="/app/profile/bank-card?from=profile">
<n-icon :component="CardOutline" />
<span>取出信息</span>
<n-icon :component="ChevronForwardOutline" />
</router-link>
<router-link class="m-profile-info-row" to="/app/profile/password">
<n-icon :component="LockClosedOutline" />
<span>密码管理</span>
<n-icon :component="ChevronForwardOutline" />
</router-link>
<router-link class="m-profile-info-row" to="/app/profile/trade-password">
<n-icon :component="LockClosedOutline" />
<span>支付密码管理</span>
<n-icon :component="ChevronForwardOutline" />
</router-link>
</div>
</div>
</AsyncState>
<form v-else-if="formType === 'bank'" class="m-plain-form m-bank-form" @submit.prevent="saveBank">
<div v-else-if="formType === 'bank'">
<section v-if="paymentAccounts.length" class="m-bound-accounts">
<h2>已绑定的取出信息</h2>
<div v-for="account in paymentAccounts" :key="account.id" class="m-bound-account m-card">
<div><span>姓名</span><strong>{{ account.holder_name }}</strong></div>
<div><span>身份证号</span><strong>{{ account.identity_no || '未填写' }}</strong></div>
<div><span>银行名</span><strong>{{ account.bank_name }}</strong></div>
<div><span>银行账户</span><strong>{{ account.account_no }}</strong></div>
<div v-if="account.digital_rmb_code"><span>人民币编码</span><strong>{{ account.digital_rmb_code }}</strong></div>
<div v-if="safeImage(account.alipay_qr_url) || safeImage(account.wechat_qr_url)" class="m-bound-account-qr">
<figure v-if="safeImage(account.alipay_qr_url)"><img :src="safeImage(account.alipay_qr_url)" alt="支付宝收款码" /><figcaption>支付宝收款码</figcaption></figure>
<figure v-if="safeImage(account.wechat_qr_url)"><img :src="safeImage(account.wechat_qr_url)" alt="微信收款码" /><figcaption>微信收款码</figcaption></figure>
</div>
</div>
</section>
<form class="m-plain-form m-bank-form" @submit.prevent="saveBank">
<h2 v-if="paymentAccounts.length" class="m-bank-form-title">添加银行卡</h2>
<label
><span>姓名</span><input v-model.trim="bank.holder_name" placeholder="请输入姓名"
/></label>
@@ -88,6 +111,7 @@
submitting ? '提交中...' : '确认绑定'
}}</button>
</form>
</div>
<form
v-else-if="formType === 'tradePassword'"
@@ -216,16 +240,18 @@
import { computed, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import {
CardOutline,
CameraOutline,
ChevronForwardOutline,
ImageOutline,
KeyOutline,
LockClosedOutline,
PersonCircleOutline,
PhonePortraitOutline,
} from '@vicons/ionicons5';
import PageHeader from '../components/PageHeader.vue';
import AsyncState from '../components/AsyncState.vue';
import { clearSession, dateTime, errorText, request, safeImage } from '../api';
import { mobileAssets } from '../assets';
const route = useRoute();
const router = useRouter();
const formType = computed(() => String(route.meta.form || ''));
@@ -244,7 +270,20 @@
const loading = ref(false);
const submitting = ref(false);
const error = ref('');
const avatarError = ref('');
const avatarUploading = ref(false);
const me = ref<any>();
interface PaymentAccountDetail {
id: number;
bank_name: string;
holder_name: string;
identity_no: string;
account_no: string;
digital_rmb_code: string;
wechat_qr_url: string;
alipay_qr_url: string;
}
const paymentAccounts = ref<PaymentAccountDetail[]>([]);
const content = ref<any>();
const resetPassword = ref('');
const bank = reactive({
@@ -274,6 +313,45 @@
loading.value = false;
}
}
async function uploadAvatar(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file || avatarUploading.value) return;
avatarError.value = '';
if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type) || file.size > 5 * 1024 * 1024) {
avatarError.value = '头像仅支持 5MB 以内的 JPG、PNG 或 WebP 图片';
return;
}
avatarUploading.value = true;
try {
const data = new FormData();
data.append('file', file);
const uploaded = await request<{ id: number; url: string }>({
url: '/v1/attachments/images',
method: 'POST',
data,
});
const avatar = await request<{ attachment_id: number; avatar_url: string }>({
url: '/v1/me/avatar',
method: 'PATCH',
data: { attachment_id: uploaded.id },
});
me.value = { ...me.value, avatar_url: avatar.avatar_url || uploaded.url };
window.$message?.success('头像更新成功');
} catch (err) {
avatarError.value = errorText(err);
} finally {
avatarUploading.value = false;
}
}
async function loadPaymentAccounts() {
try {
paymentAccounts.value = await request<PaymentAccountDetail[]>({ url: '/v1/payment-accounts/details' });
} catch (err) {
error.value = errorText(err);
}
}
async function saveBank() {
error.value = '';
const identityNo = bank.identity_no.replace(/[ -]/g, '');
@@ -302,7 +380,7 @@
try {
await request({ url: '/v1/payment-accounts', method: 'POST', data: bank });
window.$message?.success('银行卡绑定成功');
await router.replace('/app/profile/withdrawal');
await router.replace(route.query.from === 'profile' ? '/app/profile/info' : '/app/profile/withdrawal');
} catch (err) {
error.value = errorText(err);
} finally {
@@ -431,6 +509,7 @@
}
onMounted(() => {
if (formType.value === 'profile') loadProfile();
else if (formType.value === 'bank') loadPaymentAccounts();
else if (formType.value === 'content') loadContent();
});
</script>