商城c端前端

This commit is contained in:
richjohnrambo
2026-10-12 23:40:02 +08:00
parent dd729cb679
commit 96d1de4a95
105 changed files with 6762 additions and 6 deletions
+382
View File
@@ -0,0 +1,382 @@
<template>
<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
v-if="formType === 'profile'"
:loading="loading"
: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>
</div>
</AsyncState>
<form v-else-if="formType === 'bank'" class="m-plain-form" @submit.prevent="saveBank">
<label
><span>姓名</span><input v-model.trim="bank.holder_name" placeholder="请输入姓名"
/></label>
<label
><span>银行卡号</span
><input v-model.trim="bank.account_no" inputmode="numeric" placeholder="请输入银行卡号"
/></label>
<label
><span>开户行</span><input v-model.trim="bank.bank_name" placeholder="请输入开户行"
/></label>
<label
><span>银行编码</span><input v-model.trim="bank.bank_code" placeholder="例如 ICBC"
/></label>
<label
><span>预留手机号</span
><input
v-model.trim="bank.reserved_phone"
inputmode="numeric"
maxlength="11"
placeholder="请输入预留手机号(选填)"
/></label>
<label
><span>交易密码</span
><input
v-model="bank.trade_password"
type="password"
inputmode="numeric"
maxlength="6"
placeholder="请输入 6 位交易密码"
/></label>
<p class="m-form-hint">微信、支付宝二维码上传能力将在上传接口开放后提供。</p>
<p v-if="error" class="m-form-error">{{ error }}</p
><button class="m-primary-btn" :disabled="submitting">{{
submitting ? '提交中...' : '确认绑定'
}}</button>
</form>
<form
v-else-if="formType === 'tradePassword'"
class="m-password-form"
@submit.prevent="saveTradePassword"
>
<h2>设置支付密码</h2><p>支付密码用于收款账户绑定和取出确认</p>
<div class="m-code-input"
><input
v-model="trade.new_trade_password"
type="password"
inputmode="numeric"
maxlength="6"
placeholder="请输入 6 位数字"
/></div>
<label class="m-input-row"
><n-icon :component="LockClosedOutline" /><input
v-model="trade.login_password"
type="password"
placeholder="请输入登录密码确认"
/></label>
<label class="m-input-row"
><n-icon :component="KeyOutline" /><input
v-model="trade.old_trade_password"
type="password"
inputmode="numeric"
maxlength="6"
placeholder="已设置过时请输入原支付密码"
/></label>
<p v-if="error" class="m-form-error">{{ error }}</p
><button class="m-primary-btn" :disabled="submitting">{{
submitting ? '提交中...' : '确定'
}}</button>
</form>
<form
v-else-if="formType === 'loginPassword'"
class="m-password-form"
@submit.prevent="saveLoginPassword"
>
<h2>修改登录密码</h2><p>修改成功后需要重新登录</p>
<label class="m-input-row"
><n-icon :component="LockClosedOutline" /><input
v-model="loginPassword.old_password"
type="password"
autocomplete="current-password"
placeholder="请输入当前密码"
/></label>
<label class="m-input-row"
><n-icon :component="KeyOutline" /><input
v-model="loginPassword.new_password"
type="password"
autocomplete="new-password"
placeholder="请输入新密码"
/></label>
<label class="m-input-row"
><n-icon :component="KeyOutline" /><input
v-model="loginPassword.confirm_password"
type="password"
autocomplete="new-password"
placeholder="请再次输入新密码"
/></label>
<p v-if="error" class="m-form-error">{{ error }}</p>
<button class="m-primary-btn" :disabled="submitting">{{
submitting ? '提交中...' : '确定修改'
}}</button>
</form>
<form v-else-if="formType === 'verify'" class="m-plain-form m-verify" @submit.prevent="goReset">
<label
><span>手机号</span
><input
v-model.trim="verify.phone"
inputmode="numeric"
maxlength="11"
placeholder="请输入手机号"
/></label>
<label
><span>验证码</span
><input
v-model.trim="verify.code"
inputmode="numeric"
maxlength="6"
placeholder="请输入验证码"
/><button type="button" @click="notifyUnavailable">获取验证码</button></label
>
<p v-if="error" class="m-form-error">{{ error }}</p
><button class="m-primary-btn">确定</button>
</form>
<form
v-else-if="formType === 'reset'"
class="m-password-form"
@submit.prevent="notifyUnavailable"
>
<h2>重设密码</h2><p>请输入新的登录密码</p
><label class="m-input-row"
><n-icon :component="LockClosedOutline" /><input
v-model="resetPassword"
type="password"
placeholder="请输入新密码" /></label
><button class="m-primary-btn">确定</button>
</form>
<AsyncState
v-else
:loading="loading"
:error="error"
:empty="!content"
empty-text="暂无内容"
@retry="loadContent"
>
<article v-if="content" class="m-content"
><h1>{{ content.title }}</h1
><p class="m-content-summary">{{ content.summary }}</p
><div class="m-content-body">{{ content.body_text || '暂无正文' }}</div
><time v-if="content.published_at"
>发布时间:{{ dateTime(content.published_at) }}</time
></article
>
</AsyncState>
</section>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import {
ChevronForwardOutline,
KeyOutline,
LockClosedOutline,
PersonCircleOutline,
} 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 || ''));
const contentTitles: Record<string, string> = {
recharge_records: '储入记录',
withdrawal_records: '取出记录',
wallet_ledgers: '账户明细',
notices: '弹窗明细',
};
const title = computed(
() =>
content.value?.title ||
contentTitles[String(route.params.key || '')] ||
String(route.meta.title || '')
);
const loading = ref(false);
const submitting = ref(false);
const error = ref('');
const me = ref<any>();
const content = ref<any>();
const resetPassword = ref('');
const bank = reactive({
account_type: 'bank_card',
bank_code: '',
bank_name: '',
holder_name: '',
account_no: '',
reserved_phone: '',
trade_password: '',
qr_attachment_id: 0,
});
const trade = reactive({ login_password: '', old_trade_password: '', new_trade_password: '' });
const loginPassword = reactive({ old_password: '', new_password: '', confirm_password: '' });
const verify = reactive({ phone: '', code: '' });
async function loadProfile() {
loading.value = true;
error.value = '';
try {
me.value = await request({ url: '/v1/me' });
} catch (err) {
error.value = errorText(err);
} finally {
loading.value = false;
}
}
async function saveBank() {
error.value = '';
if (
!bank.holder_name ||
!/^[0-9 -]{8,36}$/.test(bank.account_no) ||
!bank.bank_name ||
!/^[0-9]{6}$/.test(bank.trade_password)
) {
error.value = '请完整填写姓名、银行卡、开户行和 6 位交易密码';
return;
}
submitting.value = true;
try {
await request({ url: '/v1/payment-accounts', method: 'POST', data: bank });
window.$message?.success('提交成功,等待后台核验');
router.back();
} catch (err) {
error.value = errorText(err);
} finally {
submitting.value = false;
}
}
async function saveTradePassword() {
error.value = '';
if (!/^\d{6}$/.test(trade.new_trade_password) || trade.login_password.length < 6) {
error.value = '请输入 6 位支付密码并填写登录密码';
return;
}
submitting.value = true;
try {
await request({
url: '/v1/me/trade-password',
method: 'PUT',
data: { ...trade, old_trade_password: trade.old_trade_password || undefined },
});
window.$message?.success('支付密码设置成功');
router.back();
} catch (err) {
error.value = errorText(err);
} finally {
submitting.value = false;
}
}
async function saveLoginPassword() {
error.value = '';
if (loginPassword.old_password.length < 6 || loginPassword.new_password.length < 6) {
error.value = '登录密码至少为 6 位';
return;
}
if (loginPassword.new_password !== loginPassword.confirm_password) {
error.value = '两次输入的新密码不一致';
return;
}
submitting.value = true;
try {
await request({
url: '/v1/me/password',
method: 'PUT',
data: {
old_password: loginPassword.old_password,
new_password: loginPassword.new_password,
},
});
clearSession();
window.$message?.success('密码修改成功,请重新登录');
await router.replace('/app/auth');
} catch (err) {
error.value = errorText(err);
} finally {
submitting.value = false;
}
}
function notifyUnavailable() {
window.$message?.warning('短信验证接口尚未开放');
}
function goReset() {
if (!/^1\d{10}$/.test(verify.phone) || !/^\d{4,6}$/.test(verify.code)) {
error.value = '请输入正确的手机号和验证码';
return;
}
notifyUnavailable();
}
async function loadContent() {
loading.value = true;
error.value = '';
const key = String(route.params.key || '');
if (['recharge_records', 'wallet_ledgers', 'notices'].includes(key)) {
loading.value = false;
return;
}
try {
if (key === 'withdrawal_records') {
const data = await request<any>({
url: '/v1/withdrawal-orders',
params: { page: 1, page_size: 20, status: 'all' },
});
content.value = {
title: '取出记录',
body_text: data.items?.length
? data.items
.map((x: any) => `${dateTime(x.created_at)} ${x.status_text || x.status}`)
.join('\n')
: '暂无取出记录',
};
} else {
content.value = await request({ url: `/v1/contents/${key}` });
}
} catch (err) {
error.value = errorText(err);
} finally {
loading.value = false;
}
}
onMounted(() => {
if (formType.value === 'profile') loadProfile();
else if (formType.value === 'content') loadContent();
});
</script>