Add admin-frontend and user-frontend standalone projects, certification/invoice/discovery features, fix auth header and theme consistency

This commit is contained in:
TradeMate Dev
2026-05-22 18:35:30 +08:00
parent 18c6cf5406
commit 52dba37f22
79 changed files with 10333 additions and 248 deletions
@@ -0,0 +1,172 @@
<template>
<view class="page">
<view class="header-card">
<text class="title">实名认证</text>
<text class="subtitle">认证后可申请发票</text>
</view>
<view v-if="loading.status" class="loading-wrap">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="cert" class="section">
<view class="status-bar" :class="cert.status">
<text class="status-icon">{{ statusIcon }}</text>
<text class="status-text">{{ statusText }}</text>
<text v-if="cert.reject_reason" class="reject-reason">原因{{ cert.reject_reason }}</text>
</view>
<view class="info-card">
<view class="info-row">
<text class="info-label">认证类型</text>
<text class="info-value">{{ cert.cert_type === 'individual' ? '个人认证' : '企业认证' }}</text>
</view>
<view class="info-row" v-if="cert.personal_name">
<text class="info-label">姓名</text>
<text class="info-value">{{ cert.personal_name }}</text>
</view>
<view class="info-row" v-if="cert.company_name">
<text class="info-label">企业名称</text>
<text class="info-value">{{ cert.company_name }}</text>
</view>
</view>
<view v-if="cert.status === 'rejected'" class="action-bar">
<button class="primary-btn" @click="resetForm">重新提交</button>
</view>
</view>
<view v-else class="section">
<view class="input-group">
<text class="input-label">认证类型</text>
<view class="type-selector">
<text class="type-option" :class="{ active: form.cert_type === 'individual' }" @click="form.cert_type = 'individual'">个人认证</text>
<text class="type-option" :class="{ active: form.cert_type === 'enterprise' }" @click="form.cert_type = 'enterprise'">企业认证</text>
</view>
</view>
<view class="input-group">
<text class="input-label">姓名</text>
<input class="input-field" v-model="form.personal_name" placeholder="输入真实姓名" />
</view>
<view class="input-group">
<text class="input-label">身份证号</text>
<input class="input-field" v-model="form.personal_id" placeholder="输入身份证号码" />
</view>
<view v-if="form.cert_type === 'enterprise'" class="enterprise-fields">
<view class="input-group">
<text class="input-label">企业名称</text>
<input class="input-field" v-model="form.company_name" placeholder="输入营业执照上的企业名称" />
</view>
<view class="input-group">
<text class="input-label">统一社会信用代码</text>
<input class="input-field" v-model="form.tax_id" placeholder="输入税号" />
</view>
</view>
<view class="notice">
<text class="notice-text">提交后预计 1-3 个工作日审核完成</text>
</view>
<button class="primary-btn" :disabled="loading.submit" @click="doSubmit">
<text v-if="loading.submit">提交中...</text>
<text v-else>提交认证</text>
</button>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { certificationApi } from '@/utils/api.js'
const loading = reactive({ status: false, submit: false })
const cert = ref(null)
const form = reactive({
cert_type: 'individual',
personal_name: '',
personal_id: '',
company_name: '',
tax_id: '',
})
const statusIcon = computed(() => ({ pending: '⏳', approved: '✅', rejected: '❌' })[cert.value?.status] || '')
const statusText = computed(() => ({ pending: '审核中', approved: '已认证', rejected: '认证未通过' })[cert.value?.status] || '')
async function loadStatus() {
loading.status = true
try {
const res = await certificationApi.status()
if (res.success && res.data) {
cert.value = res.data
form.personal_name = res.data.personal_name || ''
form.company_name = res.data.company_name || ''
}
} catch (e) {
// not certified yet
}
loading.status = false
}
function resetForm() {
cert.value = null
form.cert_type = 'individual'
form.personal_name = ''
form.personal_id = ''
form.company_name = ''
form.tax_id = ''
}
async function doSubmit() {
if (!form.personal_name) { uni.showToast({ title: '请填写姓名', icon: 'none' }); return }
if (!form.personal_id) { uni.showToast({ title: '请填写身份证号', icon: 'none' }); return }
if (form.cert_type === 'enterprise' && !form.company_name) {
uni.showToast({ title: '请填写企业名称', icon: 'none' }); return
}
loading.submit = true
try {
const res = await certificationApi.submit(form)
if (res.success) {
uni.showToast({ title: '提交成功', icon: 'success' })
loadStatus()
}
} catch (e) {
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
}
loading.submit = false
}
onMounted(loadStatus)
</script>
<style>
page { background: #f5f5f5; }
.page { padding: 16px; }
.header-card { background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 12px; padding: 20px; margin-bottom: 16px; }
.header-card .title { font-size: 20px; font-weight: 600; color: #fff; display: block; }
.header-card .subtitle { font-size: 13px; color: rgba(255,255,255,.8); display: block; margin-top: 4px; }
.section { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
.loading-wrap { text-align: center; padding: 40px; }
.loading-text { color: #999; }
.status-bar { text-align: center; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
.status-bar.pending { background: #fff7e6; }
.status-bar.approved { background: #f6ffed; }
.status-bar.rejected { background: #fff2f0; }
.status-icon { font-size: 32px; display: block; }
.status-text { font-size: 16px; font-weight: 500; display: block; margin-top: 4px; }
.reject-reason { font-size: 13px; color: #ff4d4f; display: block; margin-top: 8px; }
.info-card { }
.info-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
.info-label { color: #666; font-size: 14px; }
.info-value { color: #333; font-size: 14px; font-weight: 500; }
.action-bar { margin-top: 16px; }
.input-group { margin-bottom: 16px; }
.input-label { font-size: 14px; color: #333; display: block; margin-bottom: 6px; font-weight: 500; }
.input-field { width: 100%; height: 44px; border: 1px solid #d9d9d9; border-radius: 8px; padding: 0 12px; font-size: 14px; box-sizing: border-box; }
.type-selector { display: flex; gap: 12px; }
.type-option { flex: 1; text-align: center; padding: 10px; border: 2px solid #d9d9d9; border-radius: 8px; font-size: 14px; }
.type-option.active { border-color: #667eea; color: #667eea; font-weight: 500; }
.notice { background: #fffbe6; border: 1px solid #ffe58f; border-radius: 6px; padding: 10px; margin-bottom: 16px; }
.notice-text { font-size: 13px; color: #ad8b00; }
.primary-btn { width: 100%; height: 44px; background: #667eea; color: #fff; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; border: none; }
.primary-btn:disabled { opacity: .6; }
</style>