feat: show own withdrawal account details and enable avatar upload
This commit is contained in:
@@ -42,6 +42,7 @@ type Repository interface {
|
||||
CurrentSiteConfig(context.Context, int64) (json.RawMessage, error)
|
||||
AccountSecurity(context.Context, int64, int64) (*store.AccountSecurity, error)
|
||||
PaymentAccounts(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
||||
PaymentAccountDetails(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
||||
CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error)
|
||||
PaymentAccountForWithdrawal(context.Context, int64, int64, int64) (*store.PaymentAccount, error)
|
||||
ActiveWithdrawalChannel(context.Context, int64, []int) (*store.WithdrawalChannel, error)
|
||||
@@ -92,6 +93,19 @@ type PaymentAccount struct {
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentAccountDetail struct {
|
||||
ID int64 `json:"id"`
|
||||
BankName string `json:"bank_name"`
|
||||
HolderName string `json:"holder_name"`
|
||||
IdentityNo string `json:"identity_no"`
|
||||
AccountNo string `json:"account_no"`
|
||||
DigitalRMBCode string `json:"digital_rmb_code"`
|
||||
WechatQRURL string `json:"wechat_qr_url"`
|
||||
AlipayQRURL string `json:"alipay_qr_url"`
|
||||
VerifyStatus string `json:"verify_status"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type QuoteInput struct {
|
||||
PaymentAccountID int64 `json:"payment_account_id"`
|
||||
Amount int64 `json:"amount"`
|
||||
@@ -282,6 +296,48 @@ func (s *Service) PaymentAccounts(ctx context.Context, tenantID, memberID int64)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) PaymentAccountDetails(ctx context.Context, tenantID, memberID int64) ([]PaymentAccountDetail, error) {
|
||||
if !s.ready || s.repository == nil {
|
||||
return nil, ErrUnavailable
|
||||
}
|
||||
rows, err := s.repository.PaymentAccountDetails(ctx, tenantID, memberID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]PaymentAccountDetail, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
holderName, err := s.decrypt(row.HolderCipher)
|
||||
if err != nil {
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
accountNo, err := s.decrypt(row.AccountNoCipher)
|
||||
if err != nil {
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
identityNo := ""
|
||||
if row.IdentityNoCipher != "" {
|
||||
identityNo, err = s.decrypt(row.IdentityNoCipher)
|
||||
if err != nil {
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
}
|
||||
digitalRMBCode := ""
|
||||
if row.DigitalRMBCodeCipher != "" {
|
||||
digitalRMBCode, err = s.decrypt(row.DigitalRMBCodeCipher)
|
||||
if err != nil {
|
||||
return nil, store.ErrPaymentAccountInvalid
|
||||
}
|
||||
}
|
||||
items = append(items, PaymentAccountDetail{
|
||||
ID: row.ID, BankName: row.BankName, HolderName: holderName,
|
||||
IdentityNo: identityNo, AccountNo: accountNo, DigitalRMBCode: digitalRMBCode,
|
||||
WechatQRURL: row.WechatQRURL, AlipayQRURL: row.AlipayQRURL,
|
||||
VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID int64, input PaymentAccountInput) (*PaymentAccount, error) {
|
||||
if !s.ready {
|
||||
return nil, ErrUnavailable
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
type fakeRepository struct {
|
||||
config json.RawMessage
|
||||
account *store.PaymentAccount
|
||||
details []store.PaymentAccount
|
||||
channel *store.WithdrawalChannel
|
||||
wallet *store.WithdrawalWallet
|
||||
orders []store.WithdrawalOrder
|
||||
@@ -43,6 +44,9 @@ func (f *fakeRepository) AccountSecurity(context.Context, int64, int64) (*store.
|
||||
func (f *fakeRepository) PaymentAccounts(context.Context, int64, int64) ([]store.PaymentAccount, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeRepository) PaymentAccountDetails(context.Context, int64, int64) ([]store.PaymentAccount, error) {
|
||||
return f.details, nil
|
||||
}
|
||||
func (f *fakeRepository) CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
@@ -139,6 +143,31 @@ func TestNormalizePaymentAccountAllowsEmptyDigitalRMBCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentAccountDetailsReturnsOriginalFields(t *testing.T) {
|
||||
repository := &fakeRepository{}
|
||||
service := New(repository, &redis.Client{}, config.AuthConfig{
|
||||
FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters",
|
||||
})
|
||||
holder, _ := service.encrypt("张三")
|
||||
identity, _ := service.encrypt("11010119900101123X")
|
||||
account, _ := service.encrypt("6222000000001234")
|
||||
digital, _ := service.encrypt("123456")
|
||||
repository.details = []store.PaymentAccount{{
|
||||
ID: 7, BankName: "中国工商银行", HolderCipher: holder, IdentityNoCipher: identity,
|
||||
AccountNoCipher: account, DigitalRMBCodeCipher: digital,
|
||||
WechatQRURL: "/wechat.png", AlipayQRURL: "/alipay.png", VerifyStatus: 2,
|
||||
}}
|
||||
items, err := service.PaymentAccountDetails(context.Background(), 1, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 1 || items[0].HolderName != "张三" || items[0].IdentityNo != "11010119900101123X" ||
|
||||
items[0].AccountNo != "6222000000001234" || items[0].DigitalRMBCode != "123456" ||
|
||||
items[0].WechatQRURL != "/wechat.png" || items[0].AlipayQRURL != "/alipay.png" {
|
||||
t.Fatalf("unexpected account details: %#v", items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePaymentAccountDoesNotStoreEmptyDigitalRMBCode(t *testing.T) {
|
||||
repository := &paymentAccountRepository{}
|
||||
service := New(repository, &redis.Client{}, config.AuthConfig{
|
||||
|
||||
@@ -55,6 +55,7 @@ func (a *API) Register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/v1/manual-products", a.manualProducts)
|
||||
mux.HandleFunc("/v1/manual-products/", a.manualProduct)
|
||||
mux.HandleFunc("/v1/payment-accounts", a.paymentAccounts)
|
||||
mux.HandleFunc("/v1/payment-accounts/details", a.paymentAccountDetails)
|
||||
mux.HandleFunc("/v1/attachments/images", a.uploadImage)
|
||||
mux.HandleFunc("/v1/wallet/ledgers", a.walletLedgers)
|
||||
mux.HandleFunc("/v1/recharge/channels", a.rechargeChannels)
|
||||
|
||||
@@ -41,6 +41,24 @@ func (a *API) paymentAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
writeSuccess(w, r, result)
|
||||
}
|
||||
|
||||
func (a *API) paymentAccountDetails(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
methodNotAllowed(w, r, http.MethodGet)
|
||||
return
|
||||
}
|
||||
principal, ok := a.authenticateFinanceMember(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
result, err := a.finance.PaymentAccountDetails(r.Context(), principal.TenantID, principal.MemberID)
|
||||
if err != nil {
|
||||
writeFinanceError(w, r, err)
|
||||
return
|
||||
}
|
||||
writeSuccess(w, r, result)
|
||||
}
|
||||
|
||||
func (a *API) walletLedgers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
methodNotAllowed(w, r, http.MethodGet)
|
||||
|
||||
@@ -30,6 +30,8 @@ type PaymentAccount struct {
|
||||
QRAttachmentID int64 `db:"qr_attachment_id"`
|
||||
WechatQRAttachmentID int64 `db:"wechat_qr_attachment_id"`
|
||||
AlipayQRAttachmentID int64 `db:"alipay_qr_attachment_id"`
|
||||
WechatQRURL string `db:"wechat_qr_url"`
|
||||
AlipayQRURL string `db:"alipay_qr_url"`
|
||||
VerifyStatus int `db:"verify_status"`
|
||||
IsDefault int `db:"is_default"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
@@ -154,6 +156,29 @@ func (s *Store) PaymentAccounts(ctx context.Context, tenantID, memberID int64) (
|
||||
return items, err
|
||||
}
|
||||
|
||||
// PaymentAccountDetails returns encrypted fields only for the authenticated account owner.
|
||||
func (s *Store) PaymentAccountDetails(ctx context.Context, tenantID, memberID int64) ([]PaymentAccount, error) {
|
||||
items := make([]PaymentAccount, 0)
|
||||
err := s.db.SelectContext(ctx, &items, `SELECT a.id,a.account_type,a.bank_code,a.bank_name,
|
||||
COALESCE(a.holder_cipher,'') AS holder_cipher,
|
||||
COALESCE(a.identity_no_cipher,'') AS identity_no_cipher,
|
||||
COALESCE(a.account_no_cipher,'') AS account_no_cipher,
|
||||
COALESCE(a.digital_rmb_code_cipher,'') AS digital_rmb_code_cipher,
|
||||
COALESCE(a.wechat_qr_attachment_id,0) AS wechat_qr_attachment_id,
|
||||
COALESCE(a.alipay_qr_attachment_id,0) AS alipay_qr_attachment_id,
|
||||
COALESCE(wechat.file_url,'') AS wechat_qr_url,
|
||||
COALESCE(alipay.file_url,'') AS alipay_qr_url,
|
||||
a.verify_status,a.is_default
|
||||
FROM hg_app_member_payment_account a
|
||||
LEFT JOIN hg_sys_attachment wechat ON wechat.id=a.wechat_qr_attachment_id
|
||||
AND wechat.member_id=a.member_id AND wechat.app_id='olons_server' AND wechat.status=1
|
||||
LEFT JOIN hg_sys_attachment alipay ON alipay.id=a.alipay_qr_attachment_id
|
||||
AND alipay.member_id=a.member_id AND alipay.app_id='olons_server' AND alipay.status=1
|
||||
WHERE a.tenant_id=? AND a.member_id=? AND a.deleted_at IS NULL
|
||||
ORDER BY a.is_default ASC,a.id DESC`, tenantID, memberID)
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (s *Store) CreatePaymentAccount(ctx context.Context, input PaymentAccountCreation) (*PaymentAccount, error) {
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
|
||||
+114
-3
@@ -1772,9 +1772,120 @@ body {
|
||||
background: #fff;
|
||||
}
|
||||
.m-subpage--profile {
|
||||
background-color: #fffaf5;
|
||||
background-size: 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
background-color: #f2f6fc;
|
||||
}
|
||||
.m-profile-info {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 20px 12px 32px;
|
||||
}
|
||||
.m-profile-info-card {
|
||||
padding: 0 18px;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
.m-profile-info-row {
|
||||
min-height: 66px;
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto 20px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid #e9eef5;
|
||||
color: #303846;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
.m-profile-info-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.m-profile-info-row > .n-icon:first-child {
|
||||
font-size: 24px;
|
||||
}
|
||||
.m-profile-info-row > .n-icon:last-child,
|
||||
.m-profile-avatar-row > .n-icon:nth-last-child(2) {
|
||||
grid-column: 4;
|
||||
font-size: 21px;
|
||||
}
|
||||
.m-profile-info-row > b {
|
||||
grid-column: 3 / 5;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.m-profile-avatar-row {
|
||||
min-height: 94px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.m-profile-avatar-row input {
|
||||
display: none;
|
||||
}
|
||||
.m-profile-avatar-preview {
|
||||
grid-column: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.m-profile-avatar-preview > img,
|
||||
.m-profile-avatar-preview > .n-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
font-size: 52px;
|
||||
}
|
||||
.m-profile-avatar-preview small {
|
||||
color: var(--m-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.m-profile-info > .m-form-error {
|
||||
margin: -10px 6px 0;
|
||||
}
|
||||
.m-bound-accounts {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
.m-bound-accounts h2,
|
||||
.m-bank-form-title {
|
||||
margin: 0 20px 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.m-bound-account {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0 12px;
|
||||
padding: 16px;
|
||||
color: var(--m-ink);
|
||||
}
|
||||
.m-bound-account > div:not(.m-bound-account-qr) {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.m-bound-account span {
|
||||
color: var(--m-muted);
|
||||
}
|
||||
.m-bound-account strong {
|
||||
overflow-wrap: anywhere;
|
||||
font-weight: 500;
|
||||
}
|
||||
.m-bound-account-qr {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
.m-bound-account-qr figure {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: var(--m-muted);
|
||||
}
|
||||
.m-bound-account-qr img {
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 6px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.m-bank-form-title {
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
.m-profile-edit {
|
||||
text-align: center;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -86,7 +86,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: 'profile/bank-card',
|
||||
name: 'MobileBankCard',
|
||||
component: () => import('../mobile/views/FormView.vue'),
|
||||
meta: { title: '添加银行卡', form: 'bank' },
|
||||
meta: { title: '绑定信息', form: 'bank' },
|
||||
},
|
||||
{
|
||||
path: 'profile/withdrawal',
|
||||
|
||||
Reference in New Issue
Block a user