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>
|
||||
<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">
|
||||
|
||||
@@ -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