feat:项目中心和个人中心

This commit is contained in:
李伊帆
2026-09-15 17:51:59 +08:00
parent 2c7a115dd7
commit 56eb59ed5d
43 changed files with 2028 additions and 44 deletions

View File

@@ -0,0 +1,22 @@
import type { AccountSecurityItem } from '@/types/account-security'
export const ACCOUNT_SECURITY_ITEMS: AccountSecurityItem[] = [
{
key: 'password',
title: '修改密码',
subtitle: '上次修改:15天前',
status: '已设置',
},
{
key: 'phone',
title: '绑定手机',
subtitle: '186****9618',
status: '已绑定',
},
{
key: 'email',
title: '绑定邮箱',
subtitle: 'yongzh******4@163.com',
status: '已绑定',
},
]

View File

@@ -25,8 +25,13 @@ function buildBillList(): BillItem[] {
bills.push({ bills.push({
...plan, ...plan,
projectId: project.id, projectId: project.id,
billNo: plan.id === '1' && project.id === '1' ? '20260813ZZ251245' : plan.billNo, billNo: ['1', '2', '3'].includes(plan.id) && project.id === '1'
transactionNo: paidMeta?.transactionNo || plan.transactionNo, ? '20260813ZZ251245'
: plan.billNo,
transactionNo: paidMeta?.transactionNo
|| (['2', '3'].includes(plan.id) && project.id === '1'
? 'T20260819153035678K2M001'
: plan.transactionNo),
paidAt: paidMeta?.paidAt, paidAt: paidMeta?.paidAt,
paidMethod: paidMeta?.paidMethod, paidMethod: paidMeta?.paidMethod,
}) })

View File

@@ -0,0 +1 @@
export const BOUND_EMAIL_MASKED = 'yongzh******4@163.com'

View File

@@ -0,0 +1,3 @@
export const BOUND_PHONE_MASKED = '186****9618'
export const BIND_PHONE_NEW_HINT = '注:换绑新手机号之后,可以用新的手机号及当前密码登录'

View File

@@ -0,0 +1 @@
export const FEEDBACK_DESCRIPTION_MAX_LENGTH = 500

View File

@@ -0,0 +1,38 @@
import type { FinanceQuickAction } from '@/types/finance-center'
import iconBillDetails from '@/static/icon/bill-details.svg'
import iconPendingPayment from '@/static/icon/pending-payment.svg'
import iconInvoiceApply from '@/static/icon/invoice-apply.svg'
import iconReportExport from '@/static/icon/report-export.svg'
export const FINANCE_CENTER_TOTAL = 4000000
export const FINANCE_MONTH_OPTIONS = [
'2026年1月',
'2025年12月',
'2025年11月',
]
export const FINANCE_QUICK_ACTIONS: FinanceQuickAction[] = [
{
key: 'bill-list',
label: '账单明细',
icon: iconBillDetails,
path: '/pages/project-center/bill-list/index',
},
{
key: 'pending',
label: '待支付',
icon: iconPendingPayment,
path: '/pages/project-center/bill-list/index',
},
{
key: 'invoice',
label: '发票申请',
icon: iconInvoiceApply,
},
{
key: 'export',
label: '报表导出',
icon: iconReportExport,
},
]

View File

@@ -20,11 +20,11 @@ export const PERSONAL_CENTER_PROFILE: PersonalCenterProfile = {
} }
export const PERSONAL_CENTER_MENU: PersonalCenterMenuItem[] = [ export const PERSONAL_CENTER_MENU: PersonalCenterMenuItem[] = [
{ key: 'security', label: '账号安全', icon: iconSecurity }, { key: 'security', label: '账号安全', icon: iconSecurity, path: '/pages/personal-center/account-security/index' },
{ key: 'message-push', label: '消息推送', icon: iconMessagePush }, { key: 'message-push', label: '消息推送', icon: iconMessagePush },
{ key: 'feedback', label: '意见反馈', icon: iconFeedback }, { key: 'feedback', label: '意见反馈', icon: iconFeedback, path: '/pages/personal-center/feedback/index' },
{ key: 'contract-list', label: '合同列表', icon: iconContractList }, { key: 'contract-list', label: '合同列表', icon: iconContractList },
{ key: 'finance', label: '财务管理', icon: iconFinance, path: '/pages/project-center/bill-list/index' }, { key: 'finance', label: '财务中心', icon: iconFinance, path: '/pages/personal-center/finance/index' },
{ key: 'invoice', label: '发票列表', icon: iconInvoice }, { key: 'invoice', label: '发票列表', icon: iconInvoice },
{ key: 'address', label: '地址管理', icon: iconAddress }, { key: 'address', label: '地址管理', icon: iconAddress },
{ key: 'favorites', label: '收藏列表', icon: iconFavorites, path: '/pages/case-library/favorites/index' }, { key: 'favorites', label: '收藏列表', icon: iconFavorites, path: '/pages/case-library/favorites/index' },

View File

@@ -17,7 +17,7 @@ export const QUICK_ENTRY_CATALOG: QuickEntryItem[] = [
{ id: 'report', label: '快捷提报', icon: '/static/icons/quick-report.png', bgColor: '#E8FAF4' }, { id: 'report', label: '快捷提报', icon: '/static/icons/quick-report.png', bgColor: '#E8FAF4' },
{ id: 'service', label: '在线客服', icon: '/static/icons/quick-service.png', bgColor: '#F3E8FF' }, { id: 'service', label: '在线客服', icon: '/static/icons/quick-service.png', bgColor: '#F3E8FF' },
{ id: 'contract', label: '合同管理', icon: '/static/icons/quick-contract.png', bgColor: '#FEE2E2' }, { id: 'contract', label: '合同管理', icon: '/static/icons/quick-contract.png', bgColor: '#FEE2E2' },
{ id: 'finance', label: '财务管理', icon: '/static/icons/quick-finance.png', bgColor: '#E0F2FE' }, { id: 'finance', label: '财务中心', icon: '/static/icons/quick-finance.png', bgColor: '#E0F2FE' },
{ id: 'personal', label: '个人中心', icon: '/static/icons/tab-personals.png', bgColor: '#E8F0FF' }, { id: 'personal', label: '个人中心', icon: '/static/icons/tab-personals.png', bgColor: '#E8F0FF' },
{ id: 'message', label: '消息中心', icon: '/static/icons/tab-messages.png', bgColor: '#F3E8FF' }, { id: 'message', label: '消息中心', icon: '/static/icons/tab-messages.png', bgColor: '#F3E8FF' },
{ id: 'security', label: '账号安全', icon: '/static/icons/security.png', bgColor: '#E8FAF4' }, { id: 'security', label: '账号安全', icon: '/static/icons/security.png', bgColor: '#E8FAF4' },

View File

@@ -150,6 +150,54 @@
"style": { "style": {
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "pages/personal-center/account-security/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personal-center/change-password/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personal-center/bind-phone/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personal-center/bind-phone-new/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personal-center/bind-email/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personal-center/bind-email-change/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personal-center/feedback/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personal-center/finance/index",
"style": {
"navigationStyle": "custom"
}
} }
], ],
"globalStyle": { "globalStyle": {

View File

@@ -1,6 +1,7 @@
@import '@/styles/variables.scss'; @import '@/styles/variables.scss';
.login-page { .login-page {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
@@ -8,6 +9,15 @@
background: $color-bg-page; background: $color-bg-page;
} }
.login-page__guest {
position: absolute;
top: calc(var(--status-bar-height, 44px) + 24rpx);
right: 32rpx;
z-index: 10;
font-size: 28rpx;
color: $color-text-link;
}
.login-page__header { .login-page__header {
padding-top: 120rpx; padding-top: 120rpx;
text-align: center; text-align: center;

View File

@@ -47,10 +47,21 @@ function handleUserAgreement() {
url: '/pages/Login/user-agreement/index', url: '/pages/Login/user-agreement/index',
}) })
} }
function handleGuestMode() {
uni.redirectTo({ url: '/pages/home-index/index' })
}
</script> </script>
<template> <template>
<view class="login-page"> <view class="login-page">
<text
class="login-page__guest"
@tap="handleGuestMode"
>
游客模式
</text>
<view class="login-page__header"> <view class="login-page__header">
<text class="login-page__title"> <text class="login-page__title">
欢迎回来 欢迎回来

View File

@@ -23,7 +23,12 @@ const redirectUrl = computed(() => {
const title = computed(() => pageConfig.value.title) const title = computed(() => pageConfig.value.title)
const subtitle = computed(() => `${countdown.value}秒后自动跳转至${pageConfig.value.subtitleTarget}`) const subtitle = computed(() => {
if (pageType.value === 'modifySuccess')
return `${countdown.value}秒后自动返回`
return `${countdown.value}秒后自动跳转至${pageConfig.value.subtitleTarget}`
})
onLoad((options) => { onLoad((options) => {
const type = options?.type const type = options?.type
@@ -44,7 +49,10 @@ onLoad((options) => {
const id = options?.id const id = options?.id
if (id) if (id)
projectId.value = decodeURIComponent(String(id)) projectId.value = decodeURIComponent(String(id))
return
} }
if (type === 'modifySuccess')
pageType.value = 'modifySuccess'
}) })
onMounted(() => { onMounted(() => {

View File

@@ -47,10 +47,8 @@
} }
.password-strength__error-mark { .password-strength__error-mark {
font-size: 20rpx; width: 28rpx;
font-weight: 700; height: 28rpx;
color: #ffffff;
line-height: 1;
} }
.password-strength__error-text { .password-strength__error-text {

View File

@@ -45,9 +45,11 @@ const showIndicator = computed(() => props.password.length > 0)
class="password-strength__error" class="password-strength__error"
> >
<view class="password-strength__error-icon"> <view class="password-strength__error-icon">
<text class="password-strength__error-mark"> <image
<img src="/static/icons/icon-mark.png" alt="error" /> class="password-strength__error-mark"
</text> src="/static/icons/icon-mark.png"
mode="aspectFit"
/>
</view> </view>
<text class="password-strength__error-text"> <text class="password-strength__error-text">
密码等级过低,请重新输入 密码等级过低,请重新输入

View File

@@ -0,0 +1,107 @@
@import '@/styles/variables.scss';
.account-security {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-bg-page;
}
.account-security__navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 28rpx;
background: $color-bg-page;
}
.account-security__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.account-security__back-icon {
width: 40rpx;
height: 40rpx;
}
.account-security__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.account-security__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.account-security__content {
flex: 1;
padding: 24rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
}
.account-security__list {
display: flex;
flex-direction: column;
gap: 48rpx;
}
.account-security__item {
display: flex;
align-items: center;
justify-content: space-between;
}
.account-security__item-main {
flex: 1;
min-width: 0;
}
.account-security__item-title {
display: block;
font-size: 32rpx;
color: $color-text-primary;
}
.account-security__item-subtitle {
display: block;
margin-top: 12rpx;
font-size: 26rpx;
color: $color-text-secondary;
}
.account-security__item-side {
display: flex;
flex-shrink: 0;
align-items: center;
margin-left: 24rpx;
}
.account-security__item-status {
font-size: 28rpx;
color: $color-text-secondary;
}
.account-security__item-arrow {
margin-left: 8rpx;
font-size: 36rpx;
line-height: 1;
color: $color-text-placeholder;
}
.account-security__delete-btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
margin-top: 120rpx;
font-size: 30rpx;
color: #ffffff;
background: $color-primary;
border-radius: 12rpx;
}

View File

@@ -0,0 +1,103 @@
<script setup lang="ts">
import { ACCOUNT_SECURITY_ITEMS } from '@/constants/account-security'
import type { AccountSecurityItem } from '@/types/account-security'
import { navigateBackWithFallback } from '@/utils/navigate-back'
const securityItems = ACCOUNT_SECURITY_ITEMS
function handleBack() {
navigateBackWithFallback('/pages/personal-center/index')
}
function handleItemTap(item: AccountSecurityItem) {
if (item.key === 'password') {
uni.navigateTo({ url: '/pages/personal-center/change-password/index' })
return
}
if (item.key === 'phone') {
uni.navigateTo({ url: '/pages/personal-center/bind-phone/index' })
return
}
if (item.key === 'email') {
uni.navigateTo({ url: '/pages/personal-center/bind-email/index' })
return
}
uni.showToast({ title: `${item.title}开发中`, icon: 'none' })
}
function handleDeleteAccount() {
uni.showModal({
title: '注销账号',
content: '注销后账号数据将无法恢复,确认继续吗?',
confirmText: '确认注销',
confirmColor: '#2563EB',
success: (res) => {
if (res.confirm)
uni.showToast({ title: '注销申请已提交', icon: 'none' })
},
})
}
</script>
<template>
<view class="account-security">
<view class="account-security__navbar">
<view
class="account-security__back"
@tap="handleBack"
>
<image
class="account-security__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="account-security__navbar-title">
账号安全
</text>
<view class="account-security__navbar-placeholder" />
</view>
<view class="account-security__content">
<view class="account-security__list">
<view
v-for="item in securityItems"
:key="item.key"
class="account-security__item"
@tap="handleItemTap(item)"
>
<view class="account-security__item-main">
<text class="account-security__item-title">
{{ item.title }}
</text>
<text class="account-security__item-subtitle">
{{ item.subtitle }}
</text>
</view>
<view class="account-security__item-side">
<text class="account-security__item-status">
{{ item.status }}
</text>
<text class="account-security__item-arrow">
›
</text>
</view>
</view>
</view>
<view
class="account-security__delete-btn"
@tap="handleDeleteAccount"
>
注销账号
</view>
</view>
</view>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>

View File

@@ -0,0 +1,63 @@
@import '@/styles/variables.scss';
.bind-email-change {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-bg-page;
}
.bind-email-change__navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 28rpx;
background: $color-bg-page;
}
.bind-email-change__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.bind-email-change__back-icon {
width: 40rpx;
height: 40rpx;
}
.bind-email-change__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.bind-email-change__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.bind-email-change__content {
padding: 48rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
}
.bind-email-change__field {
width: 100%;
margin-bottom: 24rpx;
box-sizing: border-box;
}
.bind-email-change__submit {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
margin-top: 48rpx;
font-size: 30rpx;
color: #ffffff;
background: $color-primary;
border-radius: 12rpx;
}

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref } from 'vue'
import ForgotInput from '@/pages/Login/user-forgot-password/components/ForgotInput.vue'
import CodeInput from '@/pages/Login/user-register/components/CodeInput.vue'
import { navigateBackWithFallback } from '@/utils/navigate-back'
import { navigateToResultPage } from '@/utils/navigate-result'
const email = ref('')
const code = ref('')
function handleBack() {
navigateBackWithFallback('/pages/personal-center/bind-email/index')
}
function isValidEmail(value: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}
function handleSubmit() {
if (!email.value.trim()) {
uni.showToast({ title: '请输入邮箱', icon: 'none' })
return
}
if (!isValidEmail(email.value.trim())) {
uni.showToast({ title: '请输入正确的邮箱', icon: 'none' })
return
}
if (!code.value.trim()) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
navigateToResultPage('modifySuccess')
}
</script>
<template>
<view class="bind-email-change">
<view class="bind-email-change__navbar">
<view
class="bind-email-change__back"
@tap="handleBack"
>
<image
class="bind-email-change__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="bind-email-change__navbar-title">
绑定邮箱
</text>
<view class="bind-email-change__navbar-placeholder" />
</view>
<view class="bind-email-change__content">
<view class="bind-email-change__field">
<ForgotInput
v-model="email"
placeholder="请输入邮箱"
/>
</view>
<view class="bind-email-change__field">
<CodeInput v-model="code" />
</view>
<view
class="bind-email-change__submit"
@tap="handleSubmit"
>
验证并绑定邮箱
</view>
</view>
</view>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>

View File

@@ -0,0 +1,96 @@
@import '@/styles/variables.scss';
.bind-email {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-bg-page;
}
.bind-email__navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 28rpx;
background: $color-bg-page;
}
.bind-email__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.bind-email__back-icon {
width: 40rpx;
height: 40rpx;
}
.bind-email__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.bind-email__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.bind-email__content {
padding: 48rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
}
.bind-email__field {
width: 100%;
box-sizing: border-box;
}
.bind-email__label {
display: block;
margin-bottom: 16rpx;
font-size: 28rpx;
font-weight: 500;
color: $color-text-primary;
}
.bind-email__email {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 96rpx;
padding: 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.bind-email__email-text {
font-size: 30rpx;
color: $color-text-primary;
}
.bind-email__actions {
display: flex;
gap: 20rpx;
margin-top: 48rpx;
}
.bind-email__btn {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 88rpx;
font-size: 28rpx;
border-radius: 12rpx;
&--primary {
color: #ffffff;
background: $color-primary;
}
}

View File

@@ -0,0 +1,80 @@
<script setup lang="ts">
import { BOUND_EMAIL_MASKED } from '@/constants/bind-email'
import { navigateBackWithFallback } from '@/utils/navigate-back'
const boundEmail = BOUND_EMAIL_MASKED
function handleBack() {
navigateBackWithFallback('/pages/personal-center/account-security/index')
}
function handleDeleteEmail() {
uni.showModal({
title: '删除邮箱',
content: '删除后将无法通过邮箱接收通知,确认删除吗?',
confirmText: '确认删除',
confirmColor: '#2563EB',
success: (res) => {
if (res.confirm)
uni.showToast({ title: '邮箱已删除', icon: 'none' })
},
})
}
function handleChangeEmail() {
uni.navigateTo({ url: '/pages/personal-center/bind-email-change/index' })
}
</script>
<template>
<view class="bind-email">
<view class="bind-email__navbar">
<view
class="bind-email__back"
@tap="handleBack"
>
<image
class="bind-email__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="bind-email__navbar-title">
绑定邮箱
</text>
<view class="bind-email__navbar-placeholder" />
</view>
<view class="bind-email__content">
<view class="bind-email__field">
<text class="bind-email__label">
已绑定邮箱
</text>
<view class="bind-email__email">
<text class="bind-email__email-text">
{{ boundEmail }}
</text>
</view>
</view>
<view class="bind-email__actions">
<view
class="bind-email__btn bind-email__btn--primary"
@tap="handleDeleteEmail"
>
删除邮箱
</view>
<view
class="bind-email__btn bind-email__btn--primary"
@tap="handleChangeEmail"
>
更换绑定邮箱
</view>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>

View File

@@ -0,0 +1,79 @@
@import '@/styles/variables.scss';
.bind-phone-new {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-bg-page;
}
.bind-phone-new__navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 28rpx;
background: $color-bg-page;
}
.bind-phone-new__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.bind-phone-new__back-icon {
width: 40rpx;
height: 40rpx;
}
.bind-phone-new__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.bind-phone-new__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.bind-phone-new__content {
padding: 48rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
}
.bind-phone-new__field {
width: 100%;
margin-bottom: 32rpx;
box-sizing: border-box;
}
.bind-phone-new__label {
display: block;
margin-bottom: 16rpx;
font-size: 28rpx;
font-weight: 500;
color: $color-text-primary;
}
.bind-phone-new__hint {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
line-height: 1.6;
color: $color-text-secondary;
}
.bind-phone-new__submit {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
margin-top: 48rpx;
font-size: 30rpx;
color: #ffffff;
background: $color-primary;
border-radius: 12rpx;
}

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import { ref } from 'vue'
import { BIND_PHONE_NEW_HINT } from '@/constants/bind-phone'
import CodeInput from '@/pages/Login/user-register/components/CodeInput.vue'
import RegisterInput from '@/pages/Login/user-register/components/RegisterInput.vue'
import { navigateBackWithFallback } from '@/utils/navigate-back'
import { navigateToResultPage } from '@/utils/navigate-result'
const hint = BIND_PHONE_NEW_HINT
const newPhone = ref('')
const code = ref('')
function handleBack() {
navigateBackWithFallback('/pages/personal-center/bind-phone/index')
}
function handleSubmit() {
if (!newPhone.value.trim()) {
uni.showToast({ title: '请输入新手机号', icon: 'none' })
return
}
if (!/^1\d{10}$/.test(newPhone.value.trim())) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
if (!code.value.trim()) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
navigateToResultPage('modifySuccess')
}
</script>
<template>
<view class="bind-phone-new">
<view class="bind-phone-new__navbar">
<view
class="bind-phone-new__back"
@tap="handleBack"
>
<image
class="bind-phone-new__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="bind-phone-new__navbar-title">
绑定手机号
</text>
<view class="bind-phone-new__navbar-placeholder" />
</view>
<view class="bind-phone-new__content">
<view class="bind-phone-new__field">
<text class="bind-phone-new__label">
请输入新手机号
</text>
<RegisterInput
v-model="newPhone"
icon="/static/icons/icon-phone.png"
placeholder="请输入新手机号"
input-type="number"
/>
</view>
<view class="bind-phone-new__field">
<text class="bind-phone-new__label">
验证码
</text>
<CodeInput v-model="code" />
</view>
<text class="bind-phone-new__hint">
{{ hint }}
</text>
<view
class="bind-phone-new__submit"
@tap="handleSubmit"
>
验证并绑定手机号
</view>
</view>
</view>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>

View File

@@ -0,0 +1,87 @@
@import '@/styles/variables.scss';
.bind-phone {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-bg-page;
}
.bind-phone__navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 28rpx;
background: $color-bg-page;
}
.bind-phone__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.bind-phone__back-icon {
width: 40rpx;
height: 40rpx;
}
.bind-phone__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.bind-phone__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.bind-phone__content {
padding: 48rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
}
.bind-phone__field {
width: 100%;
margin-bottom: 32rpx;
box-sizing: border-box;
}
.bind-phone__label {
display: block;
margin-bottom: 16rpx;
font-size: 28rpx;
font-weight: 500;
color: $color-text-primary;
}
.bind-phone__phone {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.bind-phone__phone-text {
font-size: 30rpx;
color: $color-text-primary;
}
.bind-phone__submit {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
margin-top: 48rpx;
font-size: 30rpx;
color: #ffffff;
background: $color-primary;
border-radius: 12rpx;
}

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
import { ref } from 'vue'
import { BOUND_PHONE_MASKED } from '@/constants/bind-phone'
import CodeInput from '@/pages/Login/user-register/components/CodeInput.vue'
import { navigateBackWithFallback } from '@/utils/navigate-back'
const boundPhone = BOUND_PHONE_MASKED
const code = ref('')
function handleBack() {
navigateBackWithFallback('/pages/personal-center/account-security/index')
}
function handleSubmit() {
if (!code.value.trim()) {
uni.showToast({ title: '请输入验证码', icon: 'none' })
return
}
uni.navigateTo({ url: '/pages/personal-center/bind-phone-new/index' })
}
</script>
<template>
<view class="bind-phone">
<view class="bind-phone__navbar">
<view
class="bind-phone__back"
@tap="handleBack"
>
<image
class="bind-phone__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="bind-phone__navbar-title">
绑定手机号
</text>
<view class="bind-phone__navbar-placeholder" />
</view>
<view class="bind-phone__content">
<view class="bind-phone__field">
<text class="bind-phone__label">
已绑定手机号
</text>
<view class="bind-phone__phone">
<text class="bind-phone__phone-text">
{{ boundPhone }}
</text>
</view>
</view>
<view class="bind-phone__field">
<text class="bind-phone__label">
验证码
</text>
<CodeInput v-model="code" />
</view>
<view
class="bind-phone__submit"
@tap="handleSubmit"
>
验证并换绑
</view>
</view>
</view>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>

View File

@@ -0,0 +1,71 @@
@import '@/styles/variables.scss';
.change-password {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-bg-page;
}
.change-password__navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 28rpx;
background: $color-bg-page;
}
.change-password__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.change-password__back-icon {
width: 40rpx;
height: 40rpx;
}
.change-password__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.change-password__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.change-password__content {
padding: 48rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
}
.change-password__field {
width: 100%;
margin-bottom: 24rpx;
box-sizing: border-box;
&--password {
margin-bottom: 32rpx;
}
}
.change-password__strength {
width: 100%;
}
.change-password__submit {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
margin-top: 48rpx;
font-size: 30rpx;
color: #ffffff;
background: $color-primary;
border-radius: 12rpx;
}

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import { ref } from 'vue'
import PasswordField from '@/pages/Login/user-register/components/PasswordField.vue'
import PasswordStrength from '@/pages/Login/user-register/components/PasswordStrength.vue'
import { getPasswordStrength } from '@/utils/password-strength'
import { navigateBackWithFallback } from '@/utils/navigate-back'
import { navigateToResultPage } from '@/utils/navigate-result'
const oldPassword = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
function handleBack() {
navigateBackWithFallback('/pages/personal-center/account-security/index')
}
function handleSubmit() {
if (!oldPassword.value || !newPassword.value || !confirmPassword.value) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
if (newPassword.value !== confirmPassword.value) {
uni.showToast({ title: '两次密码输入不一致', icon: 'none' })
return
}
if (getPasswordStrength(newPassword.value).isTooWeak) {
uni.showToast({ title: '密码等级过低,请重新输入', icon: 'none' })
return
}
navigateToResultPage('modifySuccess')
}
</script>
<template>
<view class="change-password">
<view class="change-password__navbar">
<view
class="change-password__back"
@tap="handleBack"
>
<image
class="change-password__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="change-password__navbar-title">
修改密码
</text>
<view class="change-password__navbar-placeholder" />
</view>
<view class="change-password__content">
<view class="change-password__field">
<PasswordField
v-model="oldPassword"
placeholder="输入旧密码"
/>
</view>
<view class="change-password__field change-password__field--password">
<PasswordField
v-model="newPassword"
placeholder="设置新密码"
/>
<view class="change-password__strength">
<PasswordStrength :password="newPassword" />
</view>
</view>
<view class="change-password__field">
<PasswordField
v-model="confirmPassword"
placeholder="确认新密码"
/>
</view>
<view
class="change-password__submit"
@tap="handleSubmit"
>
确认修改
</view>
</view>
</view>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>

View File

@@ -0,0 +1,224 @@
@import '@/styles/variables.scss';
.feedback {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-bg-section;
}
.feedback__navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 20rpx;
background: $color-bg-page;
}
.feedback__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.feedback__back-icon {
width: 40rpx;
height: 40rpx;
}
.feedback__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.feedback__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.feedback__scroll {
flex: 1;
height: 0;
padding: 20rpx 24rpx;
box-sizing: border-box;
}
.feedback__card {
padding: 28rpx 24rpx;
background: $color-bg-page;
border-radius: 20rpx;
}
.feedback__input {
width: 100%;
font-size: 28rpx;
color: $color-text-primary;
box-sizing: border-box;
&--title {
height: 80rpx;
padding: 0 24rpx;
background: $color-bg-page;
border: 1rpx solid #e8eaef;
border-radius: 12rpx;
}
}
.feedback__placeholder {
color: $color-text-placeholder;
}
.feedback__textarea-wrap {
position: relative;
margin-top: 24rpx;
padding: 20rpx 24rpx 48rpx;
background: $color-bg-page;
border: 1rpx solid #e8eaef;
border-radius: 12rpx;
}
.feedback__textarea {
width: 100%;
min-height: 280rpx;
font-size: 28rpx;
line-height: 1.6;
color: $color-text-primary;
}
.feedback__counter {
position: absolute;
right: 24rpx;
bottom: 16rpx;
font-size: 22rpx;
color: $color-text-secondary;
}
.feedback__field {
margin-top: 28rpx;
}
.feedback__label-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.feedback__label {
font-size: 28rpx;
font-weight: 500;
color: $color-text-primary;
&--inline {
display: inline;
}
}
.feedback__hint {
font-size: 24rpx;
color: $color-text-secondary;
}
.feedback__attachment-row {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
padding: 10rpx 10rpx 0 0;
}
.feedback__attachment-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 160rpx;
padding: 20rpx 16rpx;
background: $color-bg-section;
border-radius: 12rpx;
}
.feedback__attachment-remove {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 32rpx;
height: 32rpx;
}
.feedback__attachment-remove-icon {
width: 32rpx;
height: 32rpx;
}
.feedback__attachment-icon {
width: 64rpx;
height: 64rpx;
}
.feedback__attachment-name {
margin-top: 12rpx;
font-size: 22rpx;
color: $color-text-primary;
text-align: center;
word-break: break-all;
}
.feedback__attachment-add {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 160rpx;
height: 160rpx;
background: #f8f9fb;
border: 1rpx dashed #d8dde6;
border-radius: 12rpx;
}
.feedback__attachment-add-icon {
font-size: 40rpx;
line-height: 1;
color: $color-text-secondary;
}
.feedback__attachment-add-text {
margin-top: 8rpx;
font-size: 20rpx;
color: $color-text-secondary;
}
.feedback__footer {
display: flex;
gap: 20rpx;
flex-shrink: 0;
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
background: $color-bg-page;
border-top: 1rpx solid #f0f0f0;
}
.feedback__footer-btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
font-size: 30rpx;
border-radius: 12rpx;
&--outline {
flex: 0.8;
color: $color-primary;
background: $color-bg-page;
border: 1rpx solid $color-primary;
}
&--primary {
flex: 1.2;
font-weight: 600;
color: #ffffff;
background: $color-primary;
}
}

View File

@@ -0,0 +1,173 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { FEEDBACK_DESCRIPTION_MAX_LENGTH } from '@/constants/feedback'
import deleteIcon from '@/static/icons/X.png'
import pdfIcon from '@/static/icons/PDF.png'
import pngIcon from '@/static/icons/PNG.png'
import { navigateBackWithFallback } from '@/utils/navigate-back'
const title = ref('')
const description = ref('')
const attachments = ref([
{ id: '1', name: '需求文档.pdf', icon: pdfIcon },
{ id: '2', name: '系统....pdf', icon: pdfIcon },
{ id: '3', name: '参考图.png', icon: pngIcon },
])
const descLength = computed(() => description.value.length)
function handleBack() {
navigateBackWithFallback('/pages/personal-center/index')
}
function handleRemoveAttachment(id: string) {
attachments.value = attachments.value.filter(item => item.id !== id)
}
function handleAddAttachment() {
uni.showToast({ title: '附件上传开发中', icon: 'none' })
}
function handleReset() {
title.value = ''
description.value = ''
attachments.value = [
{ id: '1', name: '需求文档.pdf', icon: pdfIcon },
{ id: '2', name: '系统....pdf', icon: pdfIcon },
{ id: '3', name: '参考图.png', icon: pngIcon },
]
}
function handleSubmit() {
if (!title.value.trim()) {
uni.showToast({ title: '请输入反馈标题', icon: 'none' })
return
}
if (!description.value.trim()) {
uni.showToast({ title: '请详细描述您的问题', icon: 'none' })
return
}
uni.showToast({ title: '提交成功', icon: 'none' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
</script>
<template>
<view class="feedback">
<view class="feedback__navbar">
<view
class="feedback__back"
@tap="handleBack"
>
<image
class="feedback__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="feedback__navbar-title">
意见反馈
</text>
<view class="feedback__navbar-placeholder" />
</view>
<scroll-view
class="feedback__scroll"
scroll-y
:show-scrollbar="false"
>
<view class="feedback__card">
<input
v-model="title"
class="feedback__input feedback__input--title"
placeholder="请输入反馈标题"
placeholder-class="feedback__placeholder"
>
<view class="feedback__textarea-wrap">
<textarea
v-model="description"
class="feedback__textarea"
placeholder="请详细描述您的问题..."
placeholder-class="feedback__placeholder"
:maxlength="FEEDBACK_DESCRIPTION_MAX_LENGTH"
/>
<text class="feedback__counter">
{{ descLength }}/{{ FEEDBACK_DESCRIPTION_MAX_LENGTH }}字
</text>
</view>
<view class="feedback__field">
<view class="feedback__label-row">
<text class="feedback__label feedback__label--inline">
附件上传
</text>
<text class="feedback__hint">
支持图片/文档/压缩包
</text>
</view>
<view class="feedback__attachment-row">
<view
v-for="file in attachments"
:key="file.id"
class="feedback__attachment-item"
>
<view
class="feedback__attachment-remove"
@tap="handleRemoveAttachment(file.id)"
>
<image
class="feedback__attachment-remove-icon"
:src="deleteIcon"
mode="aspectFit"
/>
</view>
<image
class="feedback__attachment-icon"
:src="file.icon"
mode="aspectFit"
/>
<text class="feedback__attachment-name">
{{ file.name }}
</text>
</view>
<view
class="feedback__attachment-add"
@tap="handleAddAttachment"
>
<text class="feedback__attachment-add-icon">
+
</text>
<text class="feedback__attachment-add-text">
添加附件
</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="feedback__footer">
<view
class="feedback__footer-btn feedback__footer-btn--primary"
@tap="handleReset"
>
重置
</view>
<view
class="feedback__footer-btn feedback__footer-btn--primary"
@tap="handleSubmit"
>
提交反馈
</view>
</view>
</view>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>

View File

@@ -0,0 +1,154 @@
@import '@/styles/variables.scss';
.finance-center {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $color-bg-section;
}
.finance-center__navbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 20rpx;
background: $color-bg-page;
}
.finance-center__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.finance-center__back-icon {
width: 40rpx;
height: 40rpx;
}
.finance-center__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.finance-center__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.finance-center__scroll {
flex: 1;
height: 0;
padding: 20rpx 24rpx calc(32rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.finance-center__summary {
padding: 32rpx 28rpx 40rpx;
background: linear-gradient(135deg, #3b7cff 0%, $color-home-header 100%);
border-radius: 20rpx;
}
.finance-center__summary-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.finance-center__summary-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
}
.finance-center__month {
display: flex;
align-items: center;
height: 52rpx;
padding: 0 20rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 26rpx;
}
.finance-center__month-text {
font-size: 24rpx;
color: #ffffff;
}
.finance-center__month-arrow {
margin-left: 8rpx;
font-size: 20rpx;
color: #ffffff;
}
.finance-center__amount {
display: block;
margin-top: 28rpx;
font-size: 56rpx;
font-weight: 700;
color: #ffffff;
letter-spacing: 1rpx;
}
.finance-center__quick-card {
margin-top: 20rpx;
padding: 28rpx 16rpx 32rpx;
background: $color-bg-page;
border-radius: 20rpx;
}
.finance-center__quick-grid {
display: flex;
justify-content: space-between;
}
.finance-center__quick-item {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
}
.finance-center__quick-icon-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 96rpx;
height: 96rpx;
background: $color-bg-section;
border-radius: 50%;
}
.finance-center__quick-icon {
width: 48rpx;
height: 48rpx;
}
.finance-center__quick-label {
margin-top: 16rpx;
font-size: 24rpx;
color: $color-text-primary;
}
.finance-center__chart-card {
margin-top: 20rpx;
padding: 28rpx 24rpx 32rpx;
background: $color-bg-page;
border-radius: 20rpx;
}
.finance-center__chart-title {
font-size: 30rpx;
font-weight: 600;
color: $color-text-primary;
}
.finance-center__chart-placeholder {
min-height: 360rpx;
margin-top: 24rpx;
background: $color-bg-section;
border-radius: 16rpx;
}

View File

@@ -0,0 +1,122 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
FINANCE_CENTER_TOTAL,
FINANCE_MONTH_OPTIONS,
FINANCE_QUICK_ACTIONS,
} from '@/constants/finance-center'
import type { FinanceQuickAction } from '@/types/finance-center'
import { navigateBackWithFallback } from '@/utils/navigate-back'
const monthOptions = FINANCE_MONTH_OPTIONS
const quickActions = FINANCE_QUICK_ACTIONS
const selectedMonthIndex = ref(0)
const selectedMonth = computed(() => monthOptions[selectedMonthIndex.value])
const totalAmountText = computed(() => `¥ ${FINANCE_CENTER_TOTAL}`)
function handleBack() {
navigateBackWithFallback('/pages/personal-center/index')
}
function handleSelectMonth() {
uni.showActionSheet({
itemList: monthOptions,
success: (res) => {
selectedMonthIndex.value = res.tapIndex
},
})
}
function handleQuickAction(item: FinanceQuickAction) {
if (item.path) {
uni.navigateTo({ url: item.path })
return
}
uni.showToast({ title: `${item.label}开发中`, icon: 'none' })
}
</script>
<template>
<view class="finance-center">
<view class="finance-center__navbar">
<view
class="finance-center__back"
@tap="handleBack"
>
<image
class="finance-center__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="finance-center__navbar-title">
财务管理
</text>
<view class="finance-center__navbar-placeholder" />
</view>
<scroll-view
class="finance-center__scroll"
scroll-y
:show-scrollbar="false"
>
<view class="finance-center__summary">
<view class="finance-center__summary-top">
<text class="finance-center__summary-label">
累计消费总额
</text>
<view
class="finance-center__month"
@tap="handleSelectMonth"
>
<text class="finance-center__month-text">
{{ selectedMonth }}
</text>
<text class="finance-center__month-arrow">
▾
</text>
</view>
</view>
<text class="finance-center__amount">
{{ totalAmountText }}
</text>
</view>
<view class="finance-center__quick-card">
<view class="finance-center__quick-grid">
<view
v-for="item in quickActions"
:key="item.key"
class="finance-center__quick-item"
@tap="handleQuickAction(item)"
>
<view class="finance-center__quick-icon-wrap">
<image
class="finance-center__quick-icon"
:src="item.icon"
mode="aspectFit"
/>
</view>
<text class="finance-center__quick-label">
{{ item.label }}
</text>
</view>
</view>
</view>
<view class="finance-center__chart-card">
<text class="finance-center__chart-title">
业务类型分布
</text>
<view class="finance-center__chart-placeholder" />
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
@import './index.scss';
</style>

View File

@@ -12,7 +12,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-shrink: 0; flex-shrink: 0;
padding: calc(var(--status-bar-height, 44px) + 12rpx) 24rpx 20rpx; padding: calc(var(--status-bar-height, 44px) + 60rpx) 24rpx 20rpx;
background: $color-bg-page; background: $color-bg-page;
} }
@@ -46,14 +46,21 @@
} }
.bill-detail__content { .bill-detail__content {
padding: 24rpx 24rpx calc(32rpx + env(safe-area-inset-bottom)); padding: 20rpx 24rpx calc(32rpx + env(safe-area-inset-bottom));
} }
.bill-detail__hero { .bill-detail--with-footer .bill-detail__content {
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
}
.bill-detail__hero-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 24rpx 0 40rpx; padding: 40rpx 24rpx 36rpx;
margin-bottom: 20rpx;
background: $color-bg-page;
border-radius: 20rpx;
} }
.bill-detail__icon { .bill-detail__icon {
@@ -68,15 +75,26 @@
color: $color-text-primary; color: $color-text-primary;
} }
.bill-detail__amount { .bill-detail__amount-row {
margin-top: 16rpx; display: flex;
align-items: baseline;
margin-top: 20rpx;
}
.bill-detail__amount-label {
font-size: 26rpx; font-size: 26rpx;
color: $color-text-secondary; color: $color-text-secondary;
} }
.bill-detail__amount-value {
font-size: 32rpx;
font-weight: 700;
color: $color-text-primary;
}
.bill-detail__card { .bill-detail__card {
padding: 28rpx 24rpx; padding: 28rpx 24rpx;
margin-bottom: 24rpx; margin-bottom: 20rpx;
background: $color-bg-page; background: $color-bg-page;
border-radius: 20rpx; border-radius: 20rpx;
@@ -146,3 +164,22 @@
font-size: 22rpx; font-size: 22rpx;
color: $color-text-secondary; color: $color-text-secondary;
} }
.bill-detail__footer {
flex-shrink: 0;
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
background: $color-bg-page;
border-top: 1rpx solid #f0f0f0;
}
.bill-detail__pay-btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
font-size: 30rpx;
font-weight: 600;
color: #ffffff;
background: $color-primary;
border-radius: 12rpx;
}

View File

@@ -2,6 +2,9 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { getBill } from '@/constants/bill-list' import { getBill } from '@/constants/bill-list'
import { PAYMENT_PLAN_STATUS_LABEL } from '@/constants/project-detail'
import billPendingIcon from '@/static/icon/bill-pending.png'
import notDueIcon from '@/static/icon/not-due.png'
import successIcon from '@/static/icons/icon-register-success.png' import successIcon from '@/static/icons/icon-register-success.png'
import type { BillItem } from '@/types/bill' import type { BillItem } from '@/types/bill'
import { navigateBackWithFallback } from '@/utils/navigate-back' import { navigateBackWithFallback } from '@/utils/navigate-back'
@@ -11,11 +14,57 @@ type PaymentMethod = 'wechat' | 'alipay'
const bill = ref<BillItem | null>(null) const bill = ref<BillItem | null>(null)
const paymentMethod = ref<PaymentMethod>('wechat') const paymentMethod = ref<PaymentMethod>('wechat')
const paymentMethodLabel = computed(() => const statusLabel = computed(() => {
paymentMethod.value === 'wechat' ? '微信支付' : '支付宝', if (!bill.value)
return ''
return PAYMENT_PLAN_STATUS_LABEL[bill.value.status] || ''
})
const heroIcon = computed(() => {
if (bill.value?.status === 'paid')
return successIcon
if (bill.value?.status === 'not_due')
return notDueIcon
if (bill.value?.status === 'pending')
return billPendingIcon
return successIcon
})
const paymentMethodLabel = computed(() => {
if (!bill.value)
return ''
if (bill.value.status === 'paid')
return paymentMethod.value === 'wechat' ? '微信支付' : '支付宝'
if (bill.value.status === 'pending' || bill.value.status === 'not_due')
return '微信支付'
return '--'
})
const paymentTime = computed(() => {
if (!bill.value)
return ''
if (bill.value.paidAt)
return bill.value.paidAt
if (bill.value.status === 'pending' || bill.value.status === 'not_due')
return '2026-01-21 15:00:00'
return '--'
})
const orderCardTitle = computed(() => {
if (bill.value?.status === 'paid' || bill.value?.status === 'pending')
return '已支付订单'
if (bill.value?.status === 'not_due')
return '账单订单'
return '账单订单'
})
const showPaymentInfo = computed(() =>
bill.value?.status === 'paid'
|| bill.value?.status === 'not_due'
|| bill.value?.status === 'pending',
) )
const paymentTime = computed(() => bill.value?.paidAt || '2026-01-21 15:00:00') const showPayFooter = computed(() => bill.value?.status === 'pending')
onLoad((options) => { onLoad((options) => {
const projectId = options?.projectId ? decodeURIComponent(String(options.projectId)) : '1' const projectId = options?.projectId ? decodeURIComponent(String(options.projectId)) : '1'
@@ -37,12 +86,22 @@ function handleBack() {
function formatAmount(amount: number) { function formatAmount(amount: number) {
return `¥ ${amount}` return `¥ ${amount}`
} }
function handlePayNow() {
if (!bill.value)
return
uni.navigateTo({
url: `/pages/project-center/payment/index?projectId=${encodeURIComponent(bill.value.projectId)}&planId=${encodeURIComponent(bill.value.id)}`,
})
}
</script> </script>
<template> <template>
<view <view
v-if="bill" v-if="bill"
class="bill-detail" class="bill-detail"
:class="{ 'bill-detail--with-footer': showPayFooter }"
> >
<view class="bill-detail__navbar"> <view class="bill-detail__navbar">
<view <view
@@ -67,21 +126,31 @@ function formatAmount(amount: number) {
:show-scrollbar="false" :show-scrollbar="false"
> >
<view class="bill-detail__content"> <view class="bill-detail__content">
<view class="bill-detail__hero"> <view class="bill-detail__hero-card">
<image <image
class="bill-detail__icon" class="bill-detail__icon"
:src="successIcon" :src="heroIcon"
mode="aspectFit" mode="aspectFit"
/> />
<text class="bill-detail__status"> <text class="bill-detail__status">
已支付 {{ statusLabel }}
</text>
<text class="bill-detail__amount">
付款金额:{{ formatAmount(bill.amount) }}
</text> </text>
<view class="bill-detail__amount-row">
<text class="bill-detail__amount-label">
付款金额:
</text>
<text class="bill-detail__amount-value">
{{ formatAmount(bill.amount) }}
</text>
</view>
</view> </view>
<view class="bill-detail__card"> <view
v-if="showPaymentInfo"
class="bill-detail__card"
>
<text class="bill-detail__card-title"> <text class="bill-detail__card-title">
支付信息 支付信息
</text> </text>
@@ -113,7 +182,7 @@ function formatAmount(amount: number) {
<view class="bill-detail__card"> <view class="bill-detail__card">
<text class="bill-detail__card-title"> <text class="bill-detail__card-title">
已支付订单 {{ orderCardTitle }}
</text> </text>
<view class="bill-detail__order-row"> <view class="bill-detail__order-row">
<text class="bill-detail__order-title"> <text class="bill-detail__order-title">
@@ -132,6 +201,18 @@ function formatAmount(amount: number) {
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
<view
v-if="showPayFooter"
class="bill-detail__footer"
>
<view
class="bill-detail__pay-btn"
@tap="handlePayNow"
>
立即支付
</view>
</view>
</view> </view>
</template> </template>

View File

@@ -132,16 +132,19 @@
&--paid { &--paid {
color: #ffffff; color: #ffffff;
background: $color-primary; background: $color-primary;
border-radius: 40rpx;
} }
&--pending { &--pending {
color: #ffffff; color: #ffffff;
background: #ff9a3d; background: #ff9a3d;
border-radius: 40rpx;
} }
&--not_due { &--not_due {
color: $color-primary; color: #ffffff;
background: $color-primary-light; background: #25eb9f;
border-radius: 40rpx;
} }
} }

View File

@@ -35,16 +35,12 @@ function formatAmount(amount: number) {
} }
function handleBillTap(item: BillItem) { function handleBillTap(item: BillItem) {
if (item.status === 'pending') { if (item.status === 'pending' || item.status === 'paid' || item.status === 'not_due') {
const methodQuery = item.status === 'paid'
? `&method=${encodeURIComponent(item.paidMethod || 'wechat')}`
: ''
uni.navigateTo({ uni.navigateTo({
url: `/pages/project-center/payment/index?projectId=${encodeURIComponent(item.projectId)}&planId=${encodeURIComponent(item.id)}`, url: `/pages/project-center/bill-detail/index?projectId=${encodeURIComponent(item.projectId)}&planId=${encodeURIComponent(item.id)}${methodQuery}`,
})
return
}
if (item.status === 'paid') {
uni.navigateTo({
url: `/pages/project-center/bill-detail/index?projectId=${encodeURIComponent(item.projectId)}&planId=${encodeURIComponent(item.id)}&method=${encodeURIComponent(item.paidMethod || 'wechat')}`,
}) })
} }
} }

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1789454247895" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6431" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M790.66 460.5l40.69-34.96c9.43-8.1 10.5-22.3 2.4-31.73s-22.3-10.5-31.73-2.4l-39.08 33.57-41.03-33.84c-9.59-7.91-23.77-6.54-31.67 3.04-7.91 9.59-6.54 23.77 3.04 31.67l42 34.64h-54.64c-12.43 0-22.5 10.07-22.5 22.5s10.07 22.5 22.5 22.5h59v22h-59c-12.43 0-22.5 10.07-22.5 22.5s10.07 22.5 22.5 22.5h59V595c0 12.43 10.07 22.5 22.5 22.5s22.5-10.07 22.5-22.5v-22.5h59c12.43 0 22.5-10.07 22.5-22.5s-10.07-22.5-22.5-22.5h-59v-22h59c12.43 0 22.5-10.07 22.5-22.5s-10.07-22.5-22.5-22.5h-52.98z" p-id="6432" fill="#000000"></path><path d="M914.72 378.37c-32.34-43.18-79.16-71.8-131.84-80.59-53.47-8.92-106.43 3.59-149.14 35.24-42.16 31.23-69.16 77.39-76.05 129.97-6.88 52.58 7.18 105.37 39.59 148.65 32.34 43.18 79.16 71.8 131.84 80.59 11.26 1.88 22.48 2.81 33.6 2.81 41.7 0 81.82-13.06 115.54-38.04 42.16-31.23 69.16-77.39 76.05-129.97 6.88-52.6-7.18-105.39-39.59-148.66z m-19.91 140.85c-4.75 36.29-23.32 68.1-52.27 89.55-29.51 21.86-66.28 30.48-103.55 24.26-77.19-12.88-131.83-85.67-121.8-162.26 4.75-36.29 23.32-68.1 52.27-89.55 23.26-17.23 51.02-26.23 79.98-26.23 7.79 0 15.67 0.65 23.56 1.97 77.2 12.88 131.84 85.67 121.81 162.26zM643 267c0-19.33-15.67-35-35-35H289c-19.33 0-35 15.67-35 35s15.67 35 35 35h319c19.33 0 35-15.67 35-35zM460 413H289c-19.33 0-35 15.67-35 35s15.67 35 35 35h171c19.33 0 35-15.67 35-35s-15.67-35-35-35zM460 594H289c-19.33 0-35 15.67-35 35s15.67 35 35 35h171c19.33 0 35-15.67 35-35s-15.67-35-35-35z" p-id="6433" fill="#000000"></path><path d="M853.78 702.02c-22.42-0.43-40.78 18.35-40.78 40.78v93.91l-99.56-47.3c-20.38-9.68-44.02-10.29-64.87-1.68l-160.98 66.5a19.953 19.953 0 0 1-14.33 0.36L330.34 803.8a80.045 80.045 0 0 0-55.34 0.65l-87.14 33.29c-5.24 2-10.86-1.87-10.86-7.47V188c0-22.09 17.91-40 40-40h556c22.09 0 40 17.91 40 40v58.73c0 22.43 18.36 41.21 40.78 40.78 21.73-0.42 39.22-18.16 39.22-39.99V188c0-66.27-53.73-120-120-120H217c-66.27 0-120 53.73-120 120v652.86c0 25.05 11.23 48.99 31.05 64.31a79.992 79.992 0 0 0 77.5 11.45l91.1-34.81a19.94 19.94 0 0 1 13.83-0.16l143.21 50.89a79.943 79.943 0 0 0 26.79 4.63c10.38 0 20.75-2.03 30.54-6.07l159.87-66.04a19.974 19.974 0 0 1 16.22 0.42l91.55 43.49c41.69 19.8 94.24 1.65 113.85-51.86 0.32-0.88 0.48-1.82 0.48-2.75V742.01c0.01-21.83-17.48-39.58-39.21-39.99z" p-id="6434" fill="#000000"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1789454419489" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8796" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M810.666667 213.333333H213.333333c-25.6 0-42.666667 17.066667-42.666666 42.666667 0 21.333333 17.066667 38.4 38.4 42.666667H213.333333c0-25.6 17.066667-42.666667 42.666667-42.666667h512c25.6 0 42.666667 17.066667 42.666667 42.666667 25.6 0 42.666667-17.066667 42.666666-42.666667 0-21.333333-17.066667-38.4-38.4-42.666667H810.666667z m-149.333334 213.333334c12.8 0 21.333333 8.533333 21.333334 21.333333 0 8.533333-8.533333 17.066667-17.066667 21.333333H362.666667c-12.8 0-21.333333-8.533333-21.333334-21.333333 0-8.533333 8.533333-17.066667 17.066667-21.333333h302.933333z m0 128c12.8 0 21.333333 8.533333 21.333334 21.333333 0 8.533333-8.533333 17.066667-17.066667 21.333333H362.666667c-12.8 0-21.333333-8.533333-21.333334-21.333333 0-8.533333 8.533333-17.066667 17.066667-21.333333h302.933333zM768 298.666667H256v439.466666l4.266667-4.266666c17.066667-12.8 38.4-12.8 55.466666 0l4.266667 4.266666 42.666667 46.933334 42.666666-46.933334c17.066667-17.066667 42.666667-21.333333 59.733334-4.266666l4.266666 4.266666 42.666667 46.933334 42.666667-46.933334c17.066667-17.066667 42.666667-21.333333 59.733333-4.266666l4.266667 4.266666 42.666666 46.933334 42.666667-46.933334c17.066667-17.066667 42.666667-21.333333 59.733333-4.266666l4.266667 4.266666V298.666667z m42.666667-128c46.933333 0 85.333333 38.4 85.333333 85.333333s-38.4 85.333333-85.333333 85.333333v512l-76.8-85.333333-76.8 85.333333-76.8-85.333333-68.266667 85.333333-76.8-85.333333-76.8 85.333333-76.8-85.333333L213.333333 853.333333V341.333333c-46.933333 0-85.333333-38.4-85.333333-85.333333s38.4-85.333333 85.333333-85.333333h597.333334z" p-id="8797" fill="#000000"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/static/icon/not-due.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1789454392241" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7511" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.3 668.3c-16.6 0-30 13.4-30 30v64.9c0 16.5-12 30-26.7 30h-612c-14.7 0-26.7-13.5-26.7-30V260.8c0-16.5 12-30 26.7-30h612c14.7 0 26.7 13.5 26.7 30v64.9c0 16.6 13.4 30 30 30s30-13.4 30-30v-64.9c0-49.6-38.9-90-86.7-90h-612c-47.8 0-86.7 40.4-86.7 90v502.4c0 49.6 38.9 90 86.7 90h612c47.8 0 86.7-40.4 86.7-90v-64.9c0-16.5-13.4-30-30-30zM552 512a40 40 0 1 0 80 0 40 40 0 1 0-80 0z m296.3-139.6H515.8c-49.6 0-90 40.4-90 90v99.1c0 49.6 40.4 90 90 90h332.5c49.6 0 90-40.4 90-90v-99.1c0-49.6-40.3-90-90-90z m30 189.1c0 16.5-13.5 30-30 30H515.8c-16.5 0-30-13.5-30-30v-99.1c0-16.5 13.5-30 30-30h332.5c16.5 0 30 13.5 30 30v99.1z" fill="#000000" p-id="7512"></path></svg>

After

Width:  |  Height:  |  Size: 992 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1789454436192" class="icon" viewBox="0 0 1044 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9862" xmlns:xlink="http://www.w3.org/1999/xlink" width="203.90625" height="200"><path d="M206.848 1024H51.75A51.842 51.842 0 0 1 0 972.239V51.761A51.842 51.842 0 0 1 51.75 0h739.273a51.842 51.842 0 0 1 51.75 51.761v252.296a40.321 40.321 0 1 1-80.643 0V80.666H80.643v862.691h126.205a40.321 40.321 0 0 1 0 80.643z" p-id="9863"></path><path d="M587.447 253.967H187.172a40.321 40.321 0 1 1 0-80.643h400.275a40.321 40.321 0 0 1 0 80.643z m0 221.421H187.172a40.321 40.321 0 0 1 0-80.642h400.275a40.321 40.321 0 0 1 0 80.642zM318.71 696.798h-131.54a40.321 40.321 0 1 1 0-80.642h131.54a40.321 40.321 0 1 1 0 80.642z m0.277 325.958a45.586 45.586 0 0 1-45.414-49.93c3.86-39.745 19.39-142.023 80.228-233.057 35.103-52.498 79.906-93.441 133.06-121.69C539.66 590.04 600.765 574.59 668.966 572v-60.99a64.007 64.007 0 0 1 105.342-48.87l247.688 209.487a64.019 64.019 0 0 1 0 97.75L774.307 978.876a64.007 64.007 0 0 1-105.342-48.87v-89.594c-26.739-2.189-61.058-0.334-100.93 11.958-73.822 22.73-145.963 74.687-214.44 154.373a45.552 45.552 0 0 1-34.607 16.014z m330.427-263.817a306.73 306.73 0 0 1 68.95 7.65l31.243 7.212v120.387l205.327-173.635-205.327-173.646v107.14l-41.716-1.521c-205.558-7.316-292.33 113.717-328.906 215.615 53.109-44.238 108.418-75.343 165.317-92.866a356.383 356.383 0 0 1 105.112-16.278zM969.9 733.145h0.058z" p-id="9864"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,8 @@
export type AccountSecurityItemKey = 'password' | 'phone' | 'email'
export interface AccountSecurityItem {
key: AccountSecurityItemKey
title: string
subtitle: string
status: string
}

View File

@@ -0,0 +1,8 @@
export type FinanceQuickActionKey = 'bill-list' | 'pending' | 'invoice' | 'export'
export interface FinanceQuickAction {
key: FinanceQuickActionKey
label: string
icon: string
path?: string
}

View File

@@ -1,4 +1,4 @@
export type ResultPageType = 'register' | 'resetPassword' | 'loginFailed' | 'submitSuccess' | 'afterSalesSuccess' export type ResultPageType = 'register' | 'resetPassword' | 'loginFailed' | 'submitSuccess' | 'afterSalesSuccess' | 'modifySuccess'
export interface ResultPageConfig { export interface ResultPageConfig {
title: string title: string
@@ -38,6 +38,12 @@ export const RESULT_PAGE_CONFIG: Record<ResultPageType, ResultPageConfig> = {
icon: '/static/icons/icon-register-success.png', icon: '/static/icons/icon-register-success.png',
redirectUrl: '/pages/project-center/detail/index', redirectUrl: '/pages/project-center/detail/index',
}, },
modifySuccess: {
title: '修改成功',
subtitleTarget: '',
icon: '/static/icons/icon-register-success.png',
redirectUrl: '/pages/personal-center/account-security/index',
},
} }
export const RESULT_PAGE_ROUTE = '/pages/Login/user-register-success/index' export const RESULT_PAGE_ROUTE = '/pages/Login/user-register-success/index'