feat(tender): 阶段3c 我的订单 + 企业资质中心 + 登录态导航

- /tender/orders:我的订单列表(状态/金额/下单时间)
- /tender/qualification:企业资质(信息+证件URL+提交审核+审核状态/驳回原因)
- HjcUserBar:登录/注册或 我的订单/企业资质/退出(列表/详情页已接入)
- 代理:tender/my-orders.get, enterprise.get, enterprise/save.post
This commit is contained in:
2026-09-08 22:48:17 +08:00
parent 0fbf122a57
commit 802b73b555
8 changed files with 345 additions and 3 deletions
+24
View File
@@ -0,0 +1,24 @@
<template>
<div class="flex items-center gap-4 text-sm">
<template v-if="loggedIn">
<span class="text-gray-500">{{ user?.nickname || user?.username || '企业' }}</span>
<NuxtLink to="/tender/orders" class="text-blue-600 hover:underline">我的订单</NuxtLink>
<NuxtLink to="/tender/qualification" class="text-blue-600 hover:underline">企业资质</NuxtLink>
<button class="text-gray-500 hover:text-red-600" @click="doLogout">退出</button>
</template>
<template v-else>
<NuxtLink to="/login" class="text-blue-600 hover:underline">登录</NuxtLink>
<NuxtLink to="/register" class="text-blue-600 hover:underline">企业注册</NuxtLink>
</template>
</div>
</template>
<script setup lang="ts">
const { isLoggedIn, user, logout } = useHjcAuth()
const loggedIn = ref(isLoggedIn())
function doLogout() {
logout()
return navigateTo('/tender')
}
</script>
+63
View File
@@ -0,0 +1,63 @@
<template>
<div class="container mx-auto px-4 py-10">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold text-gray-900">我的订单</h1>
<HjcUserBar />
</div>
<div v-if="!loggedIn" class="py-20 text-center">
<p class="text-gray-500">请先登录后查看订单</p>
<NuxtLink to="/login?redirect=/tender/orders" class="mt-4 inline-block rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">去登录</NuxtLink>
</div>
<div v-else-if="orders.length" class="space-y-4">
<div v-for="o in orders" :key="o.id" class="rounded-lg border border-gray-200 p-5">
<div class="flex items-start justify-between">
<div>
<p class="font-semibold text-gray-900">{{ o.projectName }}</p>
<p class="mt-1 text-sm text-gray-500">订单号{{ o.orderNo }} · 项目编号{{ o.projectNo }}</p>
</div>
<span class="rounded px-2 py-0.5 text-xs" :class="payStatusClass(o.payStatus)">{{ payStatusText(o.payStatus) }}</span>
</div>
<div class="mt-4 flex items-center justify-between text-sm">
<span class="text-gray-500">{{ fmtTime(o.createTime) }}</span>
<span class="text-blue-600 font-bold">¥{{ formatMoney(o.totalAmount) }}</span>
</div>
</div>
</div>
<div v-else-if="loaded" class="py-20 text-center text-gray-500">暂无订单</div>
<SiteLoading v-else />
</div>
</template>
<script setup lang="ts">
const { isLoggedIn } = useHjcAuth()
const loggedIn = ref(false)
const orders = shallowRef<any[]>([])
const loaded = ref(false)
function formatMoney(n?: number | string) {
return Number(n ?? 0).toFixed(2)
}
function fmtTime(s?: string) {
return s ? s.slice(0, 19).replace('T', ' ') : '-'
}
function payStatusText(s?: number) {
return ({ 0: '待支付', 1: '支付成功', 2: '支付失败', 3: '已退款' } as any)[s ?? 0] ?? '-'
}
function payStatusClass(s?: number) {
if (s === 1) return 'bg-green-100 text-green-700'
if (s === 3) return 'bg-gray-100 text-gray-500'
return 'bg-amber-100 text-amber-700'
}
loggedIn.value = isLoggedIn()
if (loggedIn.value) {
const res: any = await $fetch('/api/tender/my-orders')
orders.value = Array.isArray(res) ? res : []
loaded.value = true
} else {
loaded.value = true
}
</script>
+142
View File
@@ -0,0 +1,142 @@
<template>
<div class="container mx-auto px-4 py-10">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold text-gray-900">企业资质</h1>
<HjcUserBar />
</div>
<div v-if="!loggedIn" class="py-20 text-center">
<p class="text-gray-500">请先登录后管理企业资质</p>
<NuxtLink to="/login?redirect=/tender/qualification" class="mt-4 inline-block rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">去登录</NuxtLink>
</div>
<div v-else class="mx-auto max-w-2xl rounded-lg border border-gray-200 p-6">
<!-- 审核状态 -->
<div class="mb-6 rounded-md bg-gray-50 p-4 text-sm">
<span class="text-gray-600">审核状态</span>
<span :class="authStatusClass" class="rounded px-2 py-0.5 font-medium">{{ authStatusText }}</span>
<p v-if="authStatus === 2 && rejectReason" class="mt-2 text-red-500">驳回原因{{ rejectReason }}</p>
<p class="mt-1 text-xs text-gray-400">资质审核通过后方可购买标书</p>
</div>
<form class="space-y-4" @submit.prevent="submit">
<Field label="企业名称"><input v-model="form.name" class="field" /></Field>
<Field label="纳税人识别号"><input v-model="form.creditCode" class="field" /></Field>
<Field label="企业联系电话"><input v-model="form.contactPhone" class="field" /></Field>
<Field label="企业邮箱"><input v-model="form.contactEmail" type="email" class="field" /></Field>
<Field label="企业地址"><input v-model="form.address" class="field" /></Field>
<Field label="经办人姓名"><input v-model="form.agentName" class="field" /></Field>
<Field label="经办人邮箱"><input v-model="form.agentEmail" type="email" class="field" /></Field>
<Field label="经办人手机号"><input v-model="form.agentPhone" class="field" /></Field>
<Field label="授权到期时间"><input v-model="form.authorizeExpire" type="datetime-local" class="field" /></Field>
<div class="space-y-3">
<Field label="经办人身份证正面 URL"><input v-model="materials.idcard_front" class="field" placeholder="/upload/xxx.jpg" /></Field>
<Field label="经办人身份证反面 URL"><input v-model="materials.idcard_back" class="field" placeholder="/upload/xxx.jpg" /></Field>
<Field label="授权委托书 URL"><input v-model="materials.handbook" class="field" placeholder="/upload/xxx.pdf" /></Field>
<Field label="营业执照 URL"><input v-model="materials.license" class="field" placeholder="/upload/xxx.jpg" /></Field>
</div>
<button type="submit" class="w-full rounded-md bg-blue-600 py-3 text-white font-medium hover:bg-blue-700 disabled:opacity-50" :disabled="submitting">
{{ submitting ? '提交中...' : '提交审核' }}
</button>
<p v-if="error" class="text-center text-sm text-red-500">{{ error }}</p>
<p v-if="tip" class="text-center text-sm text-green-600">{{ tip }}</p>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, h } from 'vue'
const Field = defineComponent({
props: { label: String },
setup(props, { slots }) {
return () => h('div', { class: 'space-y-1' }, [
h('label', { class: 'block text-sm text-gray-600' }, props.label),
slots.default?.()
])
}
})
const { isLoggedIn } = useHjcAuth()
const loggedIn = ref(false)
const authStatus = ref(0)
const rejectReason = ref('')
const submitting = ref(false)
const error = ref('')
const tip = ref('')
const form = reactive({
name: '', creditCode: '', contactPhone: '', contactEmail: '', address: '',
agentName: '', agentEmail: '', agentPhone: '', authorizeExpire: ''
})
const materials = reactive({ idcard_front: '', idcard_back: '', handbook: '', license: '' })
const authStatusText = computed(() => ({ 0: '待审核', 1: '已通过', 2: '已驳回' } as any)[authStatus.value] ?? '待审核')
const authStatusClass = computed(() => {
if (authStatus.value === 1) return 'bg-green-100 text-green-700'
if (authStatus.value === 2) return 'bg-red-100 text-red-600'
return 'bg-amber-100 text-amber-700'
})
async function load() {
const res: any = await $fetch('/api/tender/enterprise')
if (res) {
authStatus.value = res.authStatus ?? 0
rejectReason.value = res.rejectReason || ''
form.name = res.name || ''
form.creditCode = res.creditCode || ''
form.contactPhone = res.contactPhone || ''
form.contactEmail = res.contactEmail || ''
form.address = res.address || ''
form.agentName = res.agentName || ''
form.agentEmail = res.agentEmail || ''
form.agentPhone = res.agentPhone || ''
form.authorizeExpire = res.authorizeExpire ? res.authorizeExpire.slice(0, 16) : ''
const ms: any[] = res.materials || []
for (const m of ms) {
if (m.materialType && (materials as any)[m.materialType] === '') {
(materials as any)[m.materialType] = m.fileUrl || ''
}
}
}
}
async function submit() {
error.value = ''
tip.value = ''
submitting.value = true
const materialList = Object.entries(materials)
.filter(([, url]) => url)
.map(([type, url]) => ({ materialType: type, materialName: type, fileUrl: url }))
try {
await $fetch('/api/tender/enterprise/save', {
method: 'POST',
body: { ...form, materials: materialList }
})
tip.value = '提交成功,等待平台审核'
await load()
} catch (e: any) {
error.value = e?.data?.message || e?.message || '提交失败'
} finally {
submitting.value = false
}
}
loggedIn.value = isLoggedIn()
if (loggedIn.value) {
await load()
}
</script>
<style scoped>
.field {
width: 100%;
border-radius: 6px;
border: 1px solid #d1d5db;
padding: 8px 16px;
font-size: 14px;
}
</style>
@@ -3,7 +3,10 @@
<div v-if="tender" class="grid lg:grid-cols-[1fr_320px] gap-8">
<!-- 左侧项目信息 -->
<div>
<NuxtLink to="/tender" class="text-sm text-blue-600 hover:underline"> 返回标书中心</NuxtLink>
<div class="mb-3 flex items-center justify-between">
<NuxtLink to="/tender" class="text-sm text-blue-600 hover:underline"> 返回标书中心</NuxtLink>
<HjcUserBar />
</div>
<h1 class="mt-3 text-2xl font-bold text-gray-900">{{ tender.projectName }}</h1>
<div class="mt-2 flex flex-wrap gap-3 text-sm text-gray-500">
<span>项目编号{{ tender.projectNo }}</span>
@@ -1,7 +1,12 @@
<template>
<div class="container mx-auto px-4 py-10">
<h1 class="text-2xl font-bold text-gray-900 mb-2">标书中心</h1>
<p class="text-sm text-gray-500 mb-6">汇聚各类采购项目选择您需要的标书进行购买</p>
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">标书中心</h1>
<p class="text-sm text-gray-500">汇聚各类采购项目选择您需要的标书进行购买</p>
</div>
<HjcUserBar />
</div>
<!-- 搜索 + 分类 -->
<div class="mb-6 flex flex-col sm:flex-row gap-3">
+34
View File
@@ -0,0 +1,34 @@
import { $fetch } from 'ofetch'
import { createError, defineEventHandler, getHeader, getCookie } from 'h3'
import { useRuntimeConfig } from '#imports'
import { getTenantFromContext } from '../../utils/tenant'
/**
* 我的企业资质(含证件材料)
* GET /api/tender/enterprise
* 代理到 mp-api /api/hjc/enterprise/my;需登录态。
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const cookieToken = getCookie(event, 'hjc_token')
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
try {
const res = await $fetch('/hjc/enterprise/my', {
baseURL: config.public.modulesApiBase,
headers: {
TenantId: ctx.tenantId,
...(auth ? { Authorization: auth } : {})
},
query: { TenantId: ctx.tenantId }
}) as any
const data = res && res.data !== undefined ? res.data : res
return data
} catch (error: any) {
throw createError({
statusCode: error?.statusCode || error?.response?.status || 502,
statusMessage: error?.statusMessage || 'Failed to fetch enterprise'
})
}
})
+37
View File
@@ -0,0 +1,37 @@
import { $fetch } from 'ofetch'
import { createError, defineEventHandler, readBody, getHeader, getCookie } from 'h3'
import { useRuntimeConfig } from '#imports'
import { getTenantFromContext } from '../../utils/tenant'
/**
* 提交/更新企业资质(信息+证件)
* POST /api/tender/enterprise/save { name, creditCode, ..., materials: [] }
* 代理到 mp-api /api/hjc/enterprise/save;需登录态。
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const body = await readBody(event)
const cookieToken = getCookie(event, 'hjc_token')
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
try {
const res = await $fetch('/hjc/enterprise/save', {
baseURL: config.public.modulesApiBase,
method: 'POST',
headers: {
TenantId: ctx.tenantId,
...(auth ? { Authorization: auth } : {})
},
query: { TenantId: ctx.tenantId },
body
}) as any
const data = res && res.data !== undefined ? res.data : res
return data
} catch (error: any) {
throw createError({
statusCode: error?.statusCode || error?.response?.status || 502,
statusMessage: error?.statusMessage || 'Failed to save enterprise'
})
}
})
+34
View File
@@ -0,0 +1,34 @@
import { $fetch } from 'ofetch'
import { createError, defineEventHandler, getHeader, getCookie } from 'h3'
import { useRuntimeConfig } from '#imports'
import { getTenantFromContext } from '../../utils/tenant'
/**
* 我的订单列表
* GET /api/tender/my-orders
* 代理到 mp-api /api/hjc/order/my;需登录态。
*/
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const ctx = getTenantFromContext(event, config)
const cookieToken = getCookie(event, 'hjc_token')
const auth = getHeader(event, 'authorization') || (cookieToken ? `Bearer ${cookieToken}` : null)
try {
const res = await $fetch('/hjc/order/my', {
baseURL: config.public.modulesApiBase,
headers: {
TenantId: ctx.tenantId,
...(auth ? { Authorization: auth } : {})
},
query: { TenantId: ctx.tenantId }
}) as any
const data = res && res.data !== undefined ? res.data : res
return data
} catch (error: any) {
throw createError({
statusCode: error?.statusCode || error?.response?.status || 502,
statusMessage: error?.statusMessage || 'Failed to fetch my orders'
})
}
})