feat(quality-owner): 增加优质号主、极品专区与无忧租号保险

- 支持保证金余额支付、在线支付开通及后台手动设置审计
- 新增极品专区发布权限、赔付方案与保险结算费率快照
- 优化首页、商品卡和个人中心的优质号主身份展示
This commit is contained in:
yml2213
2026-11-04 15:04:35 +08:00
parent 812e95e7d1
commit 98174c0e33
90 changed files with 3478 additions and 64 deletions
@@ -96,6 +96,16 @@
border: none;
}
.owner-quality-tag {
height: 18px;
padding: 0 7px;
border: 1px solid #d8b4fe;
background: linear-gradient(135deg, #6d28d9 0%, #9333ea 100%) !important;
color: #fff !important;
font-size: 10px !important;
font-weight: 800;
}
.user-meta {
margin: 4px 0 0;
color: #6b7280;
@@ -664,6 +674,76 @@
font-weight: 800;
}
.quality-owner-card {
display: grid;
gap: 12px;
margin: 10px 12px;
padding: 16px;
border: 1px solid #e9e2fb;
border-radius: 14px;
background: linear-gradient(135deg, #faf8ff 0%, #ffffff 72%);
}
.quality-owner-card.is-active {
border-color: #c4b5fd;
}
.quality-owner-heading,
.quality-owner-info {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.quality-owner-heading > div,
.quality-owner-info > div {
display: grid;
gap: 3px;
}
.quality-owner-heading span,
.quality-owner-info span {
color: #7b8492;
font-size: 11px;
font-weight: 700;
}
.quality-owner-heading strong {
color: #312e81;
font-size: 15px;
font-weight: 900;
}
.quality-owner-info {
gap: 8px;
}
.quality-owner-info > div {
flex: 1;
padding: 10px;
border-radius: 8px;
background: rgba(245, 243, 255, 0.85);
}
.quality-owner-info strong {
color: #6d28d9;
font-size: 14px;
font-weight: 900;
}
.quality-owner-actions {
display: grid;
gap: 8px;
}
.quality-owner-card p {
margin: 0;
color: #7b8492;
font-size: 11px;
line-height: 1.5;
}
/* ========== 响应式适配 ========== */
@media (min-width: 520px) {
.mobile-profile-shell {
@@ -18,6 +18,18 @@ import { uploadFile } from '@/shared/api/files'
import { formatCent } from '@/shared/utils/money'
import { setPassword } from '@/features/auth/api/auth'
import { fetchUserInvitationSummary } from '@/features/invitation/api/invitation'
import {
activateQualityOwner,
fetchQualityOwnerStatus,
queryQualityOwnerPayment,
startQualityOwnerPayment,
type QualityOwnerStatus,
} from '@/features/quality-owner/api/qualityOwner'
import type { PaymentPayWay } from '@/features/orders/api/orders'
import { useMobilePayWaySelect } from '@/features/orders/composables/useMobilePayWaySelect'
import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier'
import MobilePayWaySelectPopup from '@/features/orders/components/MobilePayWaySelectPopup.vue'
import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue'
import { useDistributionSiteStore } from '@/stores/distributionSite'
@@ -33,6 +45,22 @@ const { unreadCount: notificationUnreadCount, unreadLabel: notificationUnreadLab
/** 数据项 */
const availableBalanceCent = ref(0)
const qualityOwnerStatus = ref<QualityOwnerStatus | null>(null)
const qualityOwnerLoading = ref(false)
const qualityOwnerOnlineLoading = ref(false)
const qualityOwnerPayWaySelect = useMobilePayWaySelect()
const qualityOwnerCashier = useMobilePaymentCashier({
queryPayment: () => queryQualityOwnerPayment(),
paidMessage: '保证金支付成功,优质号主已开通',
})
const {
paymentPopupVisible: qualityOwnerPaymentPopupVisible,
activePayment: activeQualityOwnerPayment,
payURL: qualityOwnerPaymentPayURL,
paymentQRCodeURL: qualityOwnerPaymentQRCodeURL,
qrGenerating: qualityOwnerPaymentQRGenerating,
checkingPayment: qualityOwnerPaymentChecking,
} = qualityOwnerCashier
const showSettings = ref(false)
const showProfileEditor = ref(false)
const savingProfile = ref(false)
@@ -121,10 +149,72 @@ onMounted(async () => {
// 保留本地会话展示,避免个人中心因网络短暂异常整页不可用。
}
void loadBalance()
void loadQualityOwnerStatus()
void loadInviteRate()
}
})
async function loadQualityOwnerStatus() {
try {
qualityOwnerStatus.value = await fetchQualityOwnerStatus()
} catch {
qualityOwnerStatus.value = null
}
}
async function handleActivateQualityOwner() {
const status = qualityOwnerStatus.value
if (!status || status.active) return
try {
await showDialog({
title: '开通优质号主',
message: `确认从可用余额中扣除 ¥${formatCent(
status.required_deposit_cent
)} 作为保证金?取消资格时会退回余额。`,
showCancelButton: true,
confirmButtonText: '确认缴纳',
cancelButtonText: '取消',
})
} catch {
return
}
qualityOwnerLoading.value = true
try {
qualityOwnerStatus.value = await activateQualityOwner()
await session.loadMe()
await loadBalance()
showToast({ message: '优质号主已开通', icon: 'passed' })
} catch (error) {
showToast({ message: readError(error, '开通失败,请确认余额充足'), icon: 'cross' })
} finally {
qualityOwnerLoading.value = false
}
}
async function handleOnlineQualityOwnerPayment() {
const payWay = await qualityOwnerPayWaySelect.select()
if (!payWay) return
qualityOwnerOnlineLoading.value = true
try {
const payment = await startQualityOwnerPayment(payWay as PaymentPayWay)
if (payment.paid) {
await handleQualityOwnerPaid()
return
}
await qualityOwnerCashier.openMobilePaymentCashier(payment, handleQualityOwnerPaid)
} catch (error) {
showToast({ message: readError(error, '发起在线支付失败'), icon: 'cross' })
} finally {
qualityOwnerOnlineLoading.value = false
}
}
async function handleQualityOwnerPaid() {
qualityOwnerStatus.value = await fetchQualityOwnerStatus()
await session.loadMe()
await loadBalance()
}
async function loadBalance() {
try {
const wallet = await fetchWalletBalance()
@@ -366,6 +456,9 @@ function resolveAvatarURL(url: string | undefined | null) {
>
未实名
</van-tag>
<van-tag v-if="qualityOwnerStatus?.active" round class="owner-quality-tag">
<van-icon name="award-o" /> 优质号主
</van-tag>
</div>
<p class="user-meta">ID: {{ displayId }} · {{ maskedPhone }}</p>
</div>
@@ -393,6 +486,65 @@ function resolveAvatarURL(url: string | undefined | null) {
</div>
</section>
<section class="quality-owner-card" :class="{ 'is-active': qualityOwnerStatus?.active }">
<div class="quality-owner-heading">
<div>
<span>优质号主</span>
<strong>{{ qualityOwnerStatus?.active ? '已开通' : '解锁极品专区发布权限' }}</strong>
</div>
<van-tag :type="qualityOwnerStatus?.active ? 'success' : 'default'" round>
{{ qualityOwnerStatus?.active ? '优质号主' : '未开通' }}
</van-tag>
</div>
<div class="quality-owner-info">
<div>
<span>保证金</span>
<strong>¥{{ formatCent(qualityOwnerStatus?.required_deposit_cent || 20000) }}</strong>
</div>
<div>
<span>极品专区保险费率</span>
<strong
>{{
((qualityOwnerStatus?.premium_insurance_fee_bps || 400) / 100).toFixed(1)
}}%</strong
>
</div>
</div>
<div v-if="!qualityOwnerStatus?.active" class="quality-owner-actions">
<van-button
block
round
type="primary"
color="#7c3aed"
:loading="qualityOwnerLoading"
:disabled="
(qualityOwnerStatus?.available_balance_cent || 0) <
(qualityOwnerStatus?.required_deposit_cent || 20000)
"
@click="handleActivateQualityOwner"
>
余额支付开通
</van-button>
<van-button
block
round
plain
type="primary"
:loading="qualityOwnerOnlineLoading"
@click="handleOnlineQualityOwnerPayment"
>
微信 / 支付宝支付开通
</van-button>
</div>
<p v-else>
{{
qualityOwnerStatus?.deposit_source === 'manual'
? '已由后台开通优质号主。'
: '保证金已冻结,取消优质号主资格时退回可用余额。'
}}
</p>
</section>
<!-- 余额卡片 - 银行卡质感暗色卡片 -->
<div class="balance-card">
<div class="balance-row">
@@ -496,6 +648,22 @@ function resolveAvatarURL(url: string | undefined | null) {
</div>
</template>
<MobilePayWaySelectPopup
v-model:show="qualityOwnerPayWaySelect.visible.value"
@choose="qualityOwnerPayWaySelect.choose"
@closed="qualityOwnerPayWaySelect.handleClosed"
/>
<MobilePaymentCashierPopup
v-model:show="qualityOwnerPaymentPopupVisible"
:payment="activeQualityOwnerPayment"
:pay-url="qualityOwnerPaymentPayURL"
:qr-code-url="qualityOwnerPaymentQRCodeURL"
:qr-generating="qualityOwnerPaymentQRGenerating"
:checking="qualityOwnerPaymentChecking"
@refresh="qualityOwnerCashier.refreshPaymentStatus(false)"
/>
<!-- 账单明细 Popup -->
<van-popup
v-model:show="showLedgers"
@@ -4,13 +4,16 @@ import {
Camera,
CircleCheckFilled,
EditPen,
Loading,
Lock,
Picture,
Postcard,
RefreshRight,
Trophy,
User,
WarningFilled,
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
@@ -18,10 +21,25 @@ import { uploadFile } from '@/shared/api/files'
import { useSessionStore } from '@/stores/session'
import { realnameStatusLabel } from '@/shared/utils/statusLabels'
import { setPassword } from '@/features/auth/api/auth'
import {
activateQualityOwner,
fetchQualityOwnerStatus,
queryQualityOwnerPayment,
startQualityOwnerPayment,
type QualityOwnerStatus,
} from '@/features/quality-owner/api/qualityOwner'
import { formatCent } from '@/shared/utils/money'
import type { PaymentPayWay } from '@/features/orders/api/orders'
import { useOrderPaymentCashier } from '@/features/orders/composables/useOrderPaymentCashier'
import PayWaySelectDialog from '@/features/orders/components/PayWaySelectDialog.vue'
const session = useSessionStore()
const router = useRouter()
const saving = ref(false)
const qualityOwnerLoading = ref(false)
const qualityOwnerOnlineLoading = ref(false)
const qualityOwnerPayWayVisible = ref(false)
const qualityOwnerStatus = ref<QualityOwnerStatus | null>(null)
const uploadingAvatar = ref(false)
const avatarFileInput = ref<HTMLInputElement | null>(null)
const form = reactive({
@@ -36,6 +54,20 @@ const passwordForm = reactive({
confirmPassword: '',
})
const qualityOwnerCashier = useOrderPaymentCashier({
notifySuccess: message => ElMessage.success(message),
notifyError: message => ElMessage.error(message),
notifyInfo: message => ElMessage.info(message),
notifyFallback: message => ElMessage.warning(message),
paidMessage: '保证金支付成功,优质号主已开通',
queryPayment: () => queryQualityOwnerPayment(),
})
const qualityOwnerPaymentVisible = qualityOwnerCashier.paymentPopupVisible
const activeQualityOwnerPayment = qualityOwnerCashier.activePayment
const qualityOwnerPaymentQRCodeURL = qualityOwnerCashier.paymentQRCodeURL
const qualityOwnerQrGenerating = qualityOwnerCashier.qrGenerating
const qualityOwnerCheckingPayment = qualityOwnerCashier.checkingPayment
const displayName = computed(() => session.displayName)
const maskedPhone = computed(() => {
if (!session.phone) return '未绑定手机号'
@@ -56,8 +88,72 @@ onMounted(async () => {
// 保留本地会话展示,避免资料页因网络短暂异常整页不可用。
}
resetForm()
void loadQualityOwnerStatus()
})
async function loadQualityOwnerStatus() {
try {
qualityOwnerStatus.value = await fetchQualityOwnerStatus()
} catch {
qualityOwnerStatus.value = null
}
}
async function handleActivateQualityOwner() {
const status = qualityOwnerStatus.value
if (!status || status.active) return
const deposit = formatCent(status.required_deposit_cent)
try {
await ElMessageBox.confirm(
`确认从可用余额中扣除 ¥${deposit} 作为优质号主保证金?保证金会冻结,取消优质号主资格时退回。`,
'开通优质号主',
{
type: 'warning',
confirmButtonText: '确认缴纳',
cancelButtonText: '取消',
}
)
} catch {
return
}
qualityOwnerLoading.value = true
try {
qualityOwnerStatus.value = await activateQualityOwner()
await session.loadMe()
ElMessage.success('优质号主已开通,可在发布时选择极品专区')
} catch (error) {
ElMessage.error(readError(error, '开通失败,请确认余额充足'))
} finally {
qualityOwnerLoading.value = false
}
}
function openQualityOwnerOnlinePayment() {
qualityOwnerPayWayVisible.value = true
}
async function chooseQualityOwnerPayWay(payWay: PaymentPayWay) {
qualityOwnerPayWayVisible.value = false
qualityOwnerOnlineLoading.value = true
try {
const payment = await startQualityOwnerPayment(payWay)
if (payment.paid) {
await handleQualityOwnerPaymentSucceeded()
return
}
await qualityOwnerCashier.openPaymentCashier(payment, handleQualityOwnerPaymentSucceeded)
} catch (error) {
ElMessage.error(readError(error, '发起在线支付失败'))
} finally {
qualityOwnerOnlineLoading.value = false
}
}
async function handleQualityOwnerPaymentSucceeded() {
qualityOwnerStatus.value = await fetchQualityOwnerStatus()
await session.loadMe()
}
function resetForm() {
form.nickname = session.nickname || session.displayName
form.avatar_url = session.avatarUrl || ''
@@ -153,14 +249,20 @@ async function savePassword() {
<template>
<section class="profile-page">
<div class="profile-hero">
<div class="profile-hero" :class="{ 'is-quality-owner': qualityOwnerStatus?.active }">
<div class="hero-avatar">
<img v-if="session.avatarUrl" :src="resolveAvatarURL(session.avatarUrl)" alt="" />
<span v-else>{{ session.avatarText }}</span>
</div>
<div class="hero-copy">
<span class="eyebrow">Profile</span>
<h1>个人资料</h1>
<div class="hero-title-row">
<h1>个人资料</h1>
<span v-if="qualityOwnerStatus?.active" class="hero-quality-chip">
<el-icon><Trophy /></el-icon>
优质号主
</span>
</div>
<p>{{ displayName }} · {{ maskedPhone }}</p>
</div>
<button
@@ -176,6 +278,24 @@ async function savePassword() {
</button>
</div>
<section v-if="qualityOwnerStatus?.active" class="quality-owner-spotlight">
<div class="quality-owner-spotlight-mark">
<el-icon :size="25"><Trophy /></el-icon>
</div>
<div class="quality-owner-spotlight-copy">
<span>PREMIUM OWNER</span>
<strong>优质号主身份已点亮</strong>
<p>
你的账号将优先展示优质号主标识,并可发布至首页极品专区,享受更高曝光与 4% 保险专享费率。
</p>
</div>
<div class="quality-owner-spotlight-benefits">
<span>极致曝光</span>
<span>身份认证</span>
<span>专享费率</span>
</div>
</section>
<div class="profile-layout">
<section class="edit-panel">
<div class="panel-head">
@@ -280,6 +400,104 @@ async function savePassword() {
</aside>
</div>
<section class="quality-owner-card" :class="{ 'is-active': qualityOwnerStatus?.active }">
<div class="quality-owner-heading">
<div>
<span class="card-kicker">Premium Owner</span>
<h2>优质号主</h2>
<p>
开通后可发布到极品专区;在极品专区购买无忧租号保险可享
{{ ((qualityOwnerStatus?.premium_insurance_fee_bps || 400) / 100).toFixed(1) }}%
专享费率。
</p>
</div>
<span class="quality-owner-status">
{{ qualityOwnerStatus?.active ? '已开通' : '未开通' }}
</span>
</div>
<div class="quality-owner-metrics">
<div>
<span>保证金</span>
<strong>¥{{ formatCent(qualityOwnerStatus?.required_deposit_cent || 20000) }}</strong>
</div>
<div>
<span>普通保险服务费</span>
<strong>{{ ((qualityOwnerStatus?.insurance_fee_bps || 800) / 100).toFixed(1) }}%</strong>
</div>
<div>
<span>极品专区保险服务费</span>
<strong
>{{
((qualityOwnerStatus?.premium_insurance_fee_bps || 400) / 100).toFixed(1)
}}%</strong
>
</div>
<div>
<span>当前可用余额</span>
<strong>¥{{ formatCent(qualityOwnerStatus?.available_balance_cent || 0) }}</strong>
</div>
</div>
<div class="quality-owner-actions">
<span v-if="qualityOwnerStatus?.active">
<template v-if="qualityOwnerStatus.deposit_source === 'manual'">已由后台开通</template>
<template v-else>保证金已冻结</template>
</span>
<span v-else>保证金取消资格时原路退回余额。</span>
<div v-if="!qualityOwnerStatus?.active" class="quality-owner-action-buttons">
<el-button
type="primary"
:loading="qualityOwnerLoading"
:disabled="
(qualityOwnerStatus?.available_balance_cent || 0) <
(qualityOwnerStatus?.required_deposit_cent || 20000)
"
@click="handleActivateQualityOwner"
>
余额支付开通
</el-button>
<el-button :loading="qualityOwnerOnlineLoading" @click="openQualityOwnerOnlinePayment">
微信 / 支付宝支付开通
</el-button>
</div>
</div>
</section>
<PayWaySelectDialog v-model="qualityOwnerPayWayVisible" @choose="chooseQualityOwnerPayWay" />
<el-dialog
v-model="qualityOwnerPaymentVisible"
title="优质号主保证金支付"
width="460px"
append-to-body
class="quality-owner-pay-dialog"
>
<div v-if="activeQualityOwnerPayment" class="quality-owner-pay-dialog-body">
<div class="quality-owner-pay-amount">
<span>支付金额</span>
<strong>¥{{ formatCent(activeQualityOwnerPayment.amount_cent) }}</strong>
</div>
<div class="quality-owner-pay-qr">
<el-icon v-if="qualityOwnerQrGenerating" class="is-loading" :size="30">
<Loading />
</el-icon>
<img
v-else-if="qualityOwnerPaymentQRCodeURL"
:src="qualityOwnerPaymentQRCodeURL"
alt="保证金支付二维码"
/>
<el-icon v-else :size="42"><Picture /></el-icon>
</div>
<p>请使用{{ activeQualityOwnerPayment.pay_way === 'WXZF' ? '微信' : '支付宝' }}扫码支付</p>
<el-button
type="primary"
:loading="qualityOwnerCheckingPayment"
@click="qualityOwnerCashier.refreshPaymentStatus(false)"
>
我已支付,刷新状态
</el-button>
</div>
</el-dialog>
<el-dialog
v-model="showPasswordDialog"
title="修改密码"
@@ -345,6 +563,19 @@ async function savePassword() {
box-shadow: 0 16px 36px rgba(23, 35, 61, 0.08);
}
.profile-hero.is-quality-owner {
border-color: #d8c9fb;
background:
radial-gradient(circle at 86% 0%, rgba(245, 158, 11, 0.15), transparent 34%),
linear-gradient(135deg, #f3edff 0%, #ffffff 58%, #fff8e8 100%);
box-shadow: 0 16px 42px rgba(109, 40, 217, 0.12);
}
.profile-hero.is-quality-owner .hero-avatar {
background: linear-gradient(135deg, #5b21b6 0%, #9333ea 62%, #f59e0b 100%);
box-shadow: 0 8px 20px rgba(109, 40, 217, 0.22);
}
.hero-avatar,
.avatar-preview {
display: grid;
@@ -389,6 +620,27 @@ async function savePassword() {
line-height: 1.2;
}
.hero-title-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.hero-quality-chip {
display: inline-flex;
align-items: center;
gap: 5px;
min-height: 26px;
padding: 0 10px;
border: 1px solid #d8b4fe;
border-radius: 999px;
background: #f5f3ff;
color: #6d28d9;
font-size: 12px;
font-weight: 900;
}
.hero-copy p {
margin: 10px 0 0;
color: #64748b;
@@ -433,6 +685,69 @@ async function savePassword() {
color: #2563eb;
}
.quality-owner-spotlight {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 16px;
margin-top: 18px;
padding: 17px 20px;
border: 1px solid #e1d7fa;
border-radius: 14px;
background: linear-gradient(110deg, #faf7ff 0%, #ffffff 62%, #fffaf0 100%);
}
.quality-owner-spotlight-mark {
display: grid;
width: 46px;
height: 46px;
place-items: center;
border-radius: 12px;
background: linear-gradient(135deg, #5b21b6 0%, #9333ea 65%, #f59e0b 100%);
color: #fff;
}
.quality-owner-spotlight-copy {
display: grid;
gap: 3px;
}
.quality-owner-spotlight-copy > span {
color: #7c3aed;
font-size: 10px;
font-weight: 900;
letter-spacing: 0.08em;
}
.quality-owner-spotlight-copy strong {
color: #2e1065;
font-size: 17px;
font-weight: 900;
}
.quality-owner-spotlight-copy p {
margin: 0;
color: #70677f;
font-size: 12px;
line-height: 1.55;
}
.quality-owner-spotlight-benefits {
display: flex;
gap: 7px;
flex-wrap: wrap;
justify-content: flex-end;
}
.quality-owner-spotlight-benefits span {
padding: 5px 9px;
border-radius: 999px;
background: #f5f3ff;
color: #6d28d9;
font-size: 11px;
font-weight: 800;
}
.profile-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
@@ -647,9 +962,178 @@ async function savePassword() {
line-height: 1.6;
}
.quality-owner-card {
margin-top: 22px;
padding: 26px 28px;
border: 1px solid #e8e4f4;
border-radius: 16px;
background: linear-gradient(135deg, #faf8ff 0%, #ffffff 68%);
box-shadow: 0 10px 28px rgba(76, 29, 149, 0.06);
}
.quality-owner-card.is-active {
border-color: #c4b5fd;
}
.quality-owner-heading,
.quality-owner-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.quality-owner-heading h2 {
margin: 6px 0 0;
color: #312e81;
font-size: 24px;
}
.quality-owner-heading p {
max-width: 680px;
margin: 8px 0 0;
color: #6b7280;
font-size: 13px;
font-weight: 700;
line-height: 1.7;
}
.quality-owner-status {
flex-shrink: 0;
padding: 6px 12px;
border-radius: 999px;
background: #f1f5f9;
color: #64748b;
font-size: 12px;
font-weight: 900;
}
.quality-owner-card.is-active .quality-owner-status {
background: #ede9fe;
color: #6d28d9;
}
.quality-owner-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-top: 20px;
}
.quality-owner-metrics div {
display: grid;
gap: 6px;
padding: 14px;
border: 1px solid #ede9fe;
border-radius: 10px;
background: rgba(255, 255, 255, 0.82);
}
.quality-owner-metrics span,
.quality-owner-actions span {
color: #7b8492;
font-size: 12px;
font-weight: 700;
}
.quality-owner-metrics strong {
color: #312e81;
font-size: 18px;
font-weight: 900;
}
.quality-owner-actions {
margin-top: 16px;
}
.quality-owner-actions :deep(.el-button--primary) {
border: none;
background: #7c3aed;
}
.quality-owner-action-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.quality-owner-action-buttons :deep(.el-button) {
margin-left: 0;
}
.quality-owner-pay-dialog-body {
display: grid;
gap: 14px;
justify-items: center;
}
.quality-owner-pay-amount {
display: flex;
align-items: baseline;
gap: 10px;
color: #64748b;
font-size: 14px;
}
.quality-owner-pay-amount strong {
color: #f97316;
font-size: 26px;
}
.quality-owner-pay-qr {
display: grid;
width: 228px;
height: 228px;
place-items: center;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #fff;
}
.quality-owner-pay-qr img {
width: 214px;
height: 214px;
}
.quality-owner-pay-dialog-body p {
margin: 0;
color: #475569;
font-size: 13px;
}
@media (max-width: 1100px) {
.profile-layout {
grid-template-columns: 1fr;
}
.quality-owner-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.quality-owner-heading,
.quality-owner-actions {
align-items: flex-start;
flex-direction: column;
}
.quality-owner-action-buttons {
justify-content: flex-start;
}
.quality-owner-metrics {
grid-template-columns: 1fr;
}
.quality-owner-spotlight {
grid-template-columns: auto minmax(0, 1fr);
}
.quality-owner-spotlight-benefits {
grid-column: 1 / -1;
justify-content: flex-start;
}
}
</style>