release: freeze demo v1.1.0 2026-10-19
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s

This commit is contained in:
WorkBuddy
2026-10-19 20:36:22 +08:00
parent 8b505d9133
commit 69ebf33e18
157 changed files with 21111 additions and 72 deletions
@@ -0,0 +1,217 @@
<template>
<PageLayout title="本地资格">
<div class="le">
<!-- 状态卡 -->
<UiLoading v-if="loading" text="正在读取资格状态…" />
<UiErrorState v-else-if="loadError" message="状态加载失败,请重试" @retry="checkStatus" />
<template v-else>
<div class="le__status" :class="`le__status--${statusTone}`">
<span class="le__status-label">当前状态</span>
<UiTag :type="statusTag">{{ statusText }}</UiTag>
</div>
<div class="le__header">
<div class="le__title">为什么需要确认本地资格?</div>
<div class="le__desc">
善缘丹阳主要服务在丹阳工作、居住或拥有丹阳户籍的适龄单身人士。
本地资格确认帮助我们更好地匹配适合你身边的伴侣。
</div>
</div>
<div class="le__section">
<div class="le__section-title">请选择你的本地资格</div>
<div class="le__types">
<div
v-for="t in eligibilityTypes"
:key="t.value"
class="le__type"
:class="{ 'le__type--active': eligibilityType === t.value }"
@click="onTypeSelect(t.value)"
>
<div class="le__type-icon">{{ t.icon }}</div>
<div class="le__type-label">{{ t.label }}</div>
</div>
</div>
</div>
<div class="le__notice">
只需声明一种资格即可。如需补充佐证材料,系统会在审核后告知你。
</div>
<div class="le__actions">
<van-button plain block @click="goToMyEligibility">查看我已有的资格</van-button>
</div>
</template>
</div>
</PageLayout>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { localEligibilityService } from '@/api/services'
import { useAppStore } from '@/stores/app'
import PageLayout from '@/layouts/PageLayout.vue'
import UiLoading from '@/components/UiLoading.vue'
import UiErrorState from '@/components/UiErrorState.vue'
import UiTag from '@/components/UiTag.vue'
const router = useRouter()
const appStore = useAppStore()
const loading = ref(true)
const loadError = ref(false)
const status = ref('not_declared')
const eligibilityType = ref('')
const eligibilityTypes = [
{ value: 'household_registration', label: '丹阳户籍', icon: '户' },
{ value: 'employment', label: '在丹阳工作', icon: '工' },
{ value: 'residence', label: '在丹阳长期居住', icon: '居' },
{ value: 'other', label: '其他符合服务条件', icon: '其' },
]
const STATUS_TEXT: Record<string, string> = {
not_declared: '尚未声明',
declared: '声明已保存',
needs_evidence: '需要补充佐证',
pending_review: '审核中',
verified: '已核验',
rejected: '未通过',
}
const statusText = computed(() => STATUS_TEXT[status.value] || '尚未声明')
const statusTag = computed<'primary' | 'success' | 'warning' | 'danger' | 'info'>(() => {
if (status.value === 'verified') return 'success'
if (status.value === 'rejected') return 'danger'
if (status.value === 'pending_review' || status.value === 'needs_evidence') return 'warning'
if (status.value === 'declared') return 'primary'
return 'info'
})
const statusTone = computed(() => (status.value === 'verified' ? 'ok' : status.value === 'rejected' ? 'bad' : 'warn'))
async function checkStatus() {
loading.value = true
loadError.value = false
try {
const result: any = await localEligibilityService.getStatus()
status.value = result?.verificationStatus || 'not_declared'
eligibilityType.value = result?.eligibilityType || ''
} catch {
loadError.value = true
} finally {
loading.value = false
}
}
function onTypeSelect(type: string) {
router.push({ path: '/local-eligibility/supplement', query: { type } })
}
function goToMyEligibility() {
if (status.value === 'needs_evidence' || status.value === 'rejected' || status.value === 'declared') {
router.push({ path: '/local-eligibility/supplement', query: { type: eligibilityType.value || '' } })
return
}
appStore.toast(statusText.value)
}
onMounted(checkStatus)
</script>
<style scoped>
.le {
padding: 16px;
}
.le__status {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: var(--mini-radius);
padding: 12px 14px;
margin-bottom: 14px;
background: var(--mini-surface);
box-shadow: var(--mini-shadow);
}
.le__status--ok {
background: rgba(47, 179, 109, 0.1);
}
.le__status--warn {
background: rgba(245, 166, 35, 0.12);
}
.le__status--bad {
background: rgba(240, 72, 62, 0.1);
}
.le__status-label {
font-size: 13px;
color: var(--mini-text-secondary);
}
.le__header {
padding: 4px 4px 12px;
}
.le__title {
font-size: 17px;
font-weight: 600;
color: var(--mini-text-strong);
}
.le__desc {
font-size: 13px;
color: var(--mini-text-secondary);
line-height: 1.8;
margin-top: 8px;
}
.le__section-title {
font-size: 14px;
font-weight: 600;
color: var(--mini-text-strong);
margin-bottom: 10px;
}
.le__types {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.le__type {
display: flex;
align-items: center;
gap: 10px;
background: var(--mini-surface);
border-radius: var(--mini-radius);
padding: 14px;
box-shadow: var(--mini-shadow);
border: 1px solid transparent;
}
.le__type--active {
border-color: var(--mini-tabbar-active);
}
.le__type-icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: var(--mini-brand-gradient);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
flex: none;
}
.le__type-label {
font-size: 13px;
color: var(--mini-text-strong);
}
.le__notice {
font-size: 12px;
color: var(--mini-text-secondary);
background: rgba(79, 140, 255, 0.06);
border-radius: var(--mini-radius);
padding: 12px 14px;
margin-top: 14px;
line-height: 1.7;
}
.le__actions {
margin-top: 18px;
}
</style>