feat(tender): 阶段3c 我的订单 + 企业资质中心 + 登录态导航
- /tender/orders:我的订单列表(状态/金额/下单时间) - /tender/qualification:企业资质(信息+证件URL+提交审核+审核状态/驳回原因) - HjcUserBar:登录/注册或 我的订单/企业资质/退出(列表/详情页已接入) - 代理:tender/my-orders.get, enterprise.get, enterprise/save.post
This commit is contained in:
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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 v-if="tender" class="grid lg:grid-cols-[1fr_320px] gap-8">
|
||||||
<!-- 左侧:项目信息 -->
|
<!-- 左侧:项目信息 -->
|
||||||
<div>
|
<div>
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<NuxtLink to="/tender" class="text-sm text-blue-600 hover:underline">← 返回标书中心</NuxtLink>
|
<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>
|
<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">
|
<div class="mt-2 flex flex-wrap gap-3 text-sm text-gray-500">
|
||||||
<span>项目编号:{{ tender.projectNo }}</span>
|
<span>项目编号:{{ tender.projectNo }}</span>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto px-4 py-10">
|
<div class="container mx-auto px-4 py-10">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">标书中心</h1>
|
<div class="mb-6 flex items-center justify-between">
|
||||||
<p class="text-sm text-gray-500 mb-6">汇聚各类采购项目,选择您需要的标书进行购买</p>
|
<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">
|
<div class="mb-6 flex flex-col sm:flex-row gap-3">
|
||||||
|
|||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user