Files
jjx-web/pages/contract/index.vue
T
ngele b680246a56 feat: 补齐 AI 智能服务模块(AI客服/合同审查/我的报告/积分管理)
从 jjx-web-sign-only 增量移植,jjx-web 原有逻辑零改动:
- 左侧菜单新增「AI 智能服务」分组
- 四个功能面板:AI 客服聊天、合同审查(上传+积分预估)、我的报告、积分管理
- data 新增 25 个字段、methods 新增 19 个方法、样式新增 467 行
- onShow 按 menuIndex 加载积分余额/报告/明细
- menuSelect 增加 e>=3 分支,AI 菜单不再触发合同列表查询
- 未带入 sign-only 专属 isSignOnly 参数
2026-10-19 20:07:20 +08:00

3122 lines
116 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="">
<top-bar></top-bar>
<common-nav></common-nav>
<view class="w-1200">
<common-swiper page="qyb" v-if="isSign"/>
<el-row :gutter="20">
<el-col :span="6">
<el-menu @select="menuSelect" :default-active="menuIndex" class="el-menu-vertical-demo">
<el-menu-item index="0">
<i class="el-icon-edit"></i>
<span slot="title">待创建</span>
</el-menu-item>
<el-menu-item index="1">
<i class="el-icon-warning-outline"></i>
<span slot="title">待签署</span>
</el-menu-item>
<el-menu-item index="2">
<i class="el-icon-circle-check"></i>
<span slot="title">已完成</span>
</el-menu-item>
<!-- <el-menu-item index="-1">-->
<!-- <i class="el-icon-delete"></i>-->
<!-- <span slot="title">已作废</span>-->
<!-- </el-menu-item>-->
</el-menu>
<view class="mt-20 bg-white flex justify-start items-center sign pointer" @click="selectSign">
<view class="icon">
<agreement theme="outline" size="18" fill="#999" strokeLinecap="square"/>
</view>
<text class="ml-20" :class="[isSign ? 'text-main' : '']">金久信签约宝</text>
</view>
<view class="bg-white flex justify-start items-center sign pointer" @click="selectSignIntro">
<view class="icon">
<agreement theme="outline" size="18" fill="#999" strokeLinecap="square"/>
</view>
<text class="ml-20" :class="[isSignIntro ? 'text-main' : '']">金久信签约宝简介</text>
</view>
<view class="bg-white flex justify-start items-center sign pointer" @click="selectSignLog">
<view class="icon">
<agreement theme="outline" size="18" fill="#999" strokeLinecap="square"/>
</view>
<text class="ml-20" :class="[isSignLog ? 'text-main' : '']">金久信签约宝消费明细</text>
</view>
<view class="bg-white flex justify-start items-center sign pointer" @click="selectSignTips">
<view class="icon">
<agreement theme="outline" size="18" fill="#999" strokeLinecap="square"/>
</view>
<text class="ml-20" :class="[isSignTips ? 'text-main' : '']">金久信签约宝重要提示</text>
</view>
<view class="bg-white flex justify-start items-center sign pointer" @click="selectSignFlow">
<view class="icon">
<agreement theme="outline" size="18" fill="#999" strokeLinecap="square"/>
</view>
<text class="ml-20" :class="[isSignFlow ? 'text-main' : '']">{{ signFlowTitle }}</text>
</view>
<view class="bg-white flex justify-start items-center sign pointer" @click="changeAuthInfo">
<view class="icon">
<agreement theme="outline" size="18" fill="#999" strokeLinecap="square"/>
</view>
<text class="ml-20">认证信息变更</text>
</view>
<div class="menu-group-title">AI 智能服务</div>
<el-menu @select="menuSelect" :default-active="menuIndex" class="el-menu-vertical-demo">
<el-menu-item index="3">
<span class="icon">🤖</span> AI 客服
</el-menu-item>
<el-menu-item index="4">
<span class="icon">📋</span> 合同审查
</el-menu-item>
<el-menu-item index="5">
<span class="icon">📈</span> 我的报告
</el-menu-item>
<el-menu-item index="6">
<span class="icon">💎</span> 积分管理
</el-menu-item>
</el-menu>
</el-col>
<el-col :span="18">
<view class="flex flex-col justify-start items-start" v-if="isSignLog">
<view class="trade-method">
<view @click="getSignLog(1)" class="trade-method-item"
:class="{active: signLogType === 'log'}">使用明细
</view>
<view @click="getSignBuyLog(1)" class="trade-method-item"
:class="{active: signLogType === 'payLog'}">付费明细
</view>
</view>
<template v-if="signLogType === 'log'">
<el-table :data="signLog.rows" style="width: 100%">
<el-table-column prop="_add_time_str" label="日期"/>
<el-table-column label="项目">
<template slot-scope="scope">
{{ parseInt(scope.row.contract.signType) === 1 ? '单据签约' : '合同签约' }}
</template>
</el-table-column>
<el-table-column prop="contract.transactionId" label="合同编号"/>
<el-table-column label="签约身份">
<template slot-scope="scope">
{{
scope.row.mineContract ? (scope.row.mineContract.role === 'third' ? '第三方' : (scope.row.mineContract.userId === scope.row.userId ? '发起方' : '接收方')) : ''
}}
</template>
</el-table-column>
<el-table-column label="扣除次数">
<template slot-scope="scope">
<p>扣除次数:1次</p>
<p v-if="scope.row.remainNum !== undefined && scope.row.remainNum !== null">
剩余次数:{{ scope.row.remainNum }}次</p>
</template>
</el-table-column>
</el-table>
<view class="flex justify-center items-center w-100p mt-20">
<el-pagination @current-change="getSignLog" background layout="prev, pager, next"
:total="signLog.total"></el-pagination>
</view>
</template>
<template v-else>
<el-table :data="signBuyLog.rows" style="width: 100%">
<el-table-column prop="_add_time_str" label="日期"/>
<el-table-column label="项目">
<template slot-scope="scope">
{{ scope.row.type === 12 ? '合同司法调证' : '金久信签约宝续费' }}
</template>
</el-table-column>
<el-table-column prop="orderNo" label="流水号"/>
<el-table-column label="缴费金额">
<template slot-scope="scope">
{{ scope.row.type === 12 ? scope.row.amount : scope.row.payAmount }}
</template>
</el-table-column>
<el-table-column label="采购次数">
<template slot-scope="scope">
<p>{{ scope.row.goods[0].goodsName }}</p>
<p v-if="scope.row.remainNum">剩余{{ scope.row.remainNum }}次</p>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<view class="flex flex-col justify-start items-start">
<el-button size="mini" @click="openInvoice(scope.row._id)" type="primary"
v-if="!scope.row.isInvoice">申请发票
</el-button>
<template v-else>
<el-button size="mini" v-if="scope.row.isInvoice === 1">开票中</el-button>
<el-button size="mini" type="success" v-if="scope.row.isInvoice === 2"
@click.native="copyInvoice(scope.row)">下载
</el-button>
</template>
<el-button size="mini" style="margin-left: 0; margin-top: 10px"
v-if="scope.row.type === 12 && scope.row.certificate && scope.row.certificate.paymentMethod === 'advance'"
@click.native="openPayVoucher(scope.row.certificate)">查看支付凭证
</el-button>
</view>
</template>
</el-table-column>
</el-table>
<view class="flex justify-center items-center w-100p mt-20">
<el-pagination @current-change="getSignBuyLog" background layout="prev, pager, next"
:total="signBuyLog.total"></el-pagination>
</view>
</template>
</view>
<view class="contract-list" v-if="!isSign && !isSignIntro && !isSignLog && !isSignFlow && !isSignTips">
<view class="trade-method">
<view @click="changeTradeMethod(null)" class="trade-method-item"
:class="{active: (formData.tradeMethod === null && formData.type === null)}">全部
</view>
<view @click="changeTradeMethod(0)" class="trade-method-item"
:class="{active: formData.tradeMethod === 0}">自主交易
</view>
<view @click="changeTradeMethod(1)" class="trade-method-item"
:class="{active: formData.tradeMethod === 1}">金久信公正
</view>
<view @click="changeType('sign')" class="trade-method-item"
:class="{active: formData.type === 'sign'}">金久信签约宝
</view>
</view>
<view class="flex justify-start items-center" v-if="menuIndex === '2'">
<view style="flex: 0 0 200px">
<el-radio-group v-model="signTypeFilter" fill="#A92924" @change="query(1)">
<el-radio-button label="2">合同签约</el-radio-button>
<el-radio-button label="1">单据签约</el-radio-button>
</el-radio-group>
</view>
<el-input class="ml-10" @keyup.enter.native="query(1)" clearable placeholder="输入签约方关键字搜索"
v-model="keyword">
<i slot="prefix" class="el-input__icon el-icon-search"></i>
<el-button @click="query(1)" type="primary" slot="append" style="border-radius: 0">搜索
</el-button>
</el-input>
</view>
<view v-for="(item,index) in tableData.rows" class="contract-box" :key="index">
<view class="contract-left">
<view class="contract-left-top">
<el-tag size="mini" v-if="item.contract.type === 'enquiry'" type="danger"
effect="plain">询价单
</el-tag>
<el-tag size="mini" v-else-if="item.contract.type === 'process'" type="warning"
effect="plain">加工单
</el-tag>
<el-tag size="mini" v-else-if="item.contract.type === 'sign'" type="success"
effect="plain">金久信签约宝
</el-tag>
<el-tag size="mini" v-else-if="item.contract.type === 'old'" type="primary"
effect="plain">旧货交易
</el-tag>
<el-tag size="mini" v-else-if="item.contract.type === 'self-support'" type="primary"
effect="plain">自营询价
</el-tag>
<template v-if="item.sheet">
<view class="ml-10">
<el-tag size="mini" v-if="item.sheet.tradeMethod === 0" type="primary"
>自主交易
</el-tag>
<!-- <el-tag size="mini" v-if="item.sheet.tradeMethod === 1" type="success"-->
<!-- >金久信公证-->
<!-- </el-tag>-->
</view>
<view class="ml-10">
<el-tag size="mini" effect="plain" v-if="item.sheet.signMethod === 0"
type="success"
>标准合同
</el-tag>
<el-tag size="mini" effect="plain" v-if="item.sheet.signMethod === 2"
type="primary"
>双方协商合同
</el-tag>
<el-tag size="mini" effect="plain" v-if="item.sheet.signMethod === 1"
type="warning"
>线下合同
</el-tag>
</view>
</template>
<view class="ml-10">
<el-tag size="mini" v-if="item.contract.tradeMethod === 1" type="success"
>金久信公证
</el-tag>
</view>
<view class="contract-product" v-if="item.contract.status > 0">
{{ item.contract.docTitle }}
</view>
<template v-else>
<text class="contract-product" v-if="item.sheet">
{{ item.sheet.title }}
</text>
</template>
<view class="contract-status">
<template v-if="item.contract.status == 1">
<view class="contract-status-tag"
v-for="(party, pIndex) in item.contract.partyList" :key="`${pIndex}_a`">
<el-tag size="mini"
v-if="party.userId !== vk.getVuex('$user.userInfo._id') && !party.sign && party.role !== 'third'"
type="warning" effect="plain">待交易方签署
</el-tag>
<el-tag size="mini"
v-if="party.userId !== vk.getVuex('$user.userInfo._id') && !party.sign && party.role === 'third'"
type="warning" effect="plain">待第三方签署
</el-tag>
<el-tag size="mini"
v-if="party.userId === vk.getVuex('$user.userInfo._id') && !party.sign"
type="primary" effect="plain">待我签署
</el-tag>
</view>
</template>
<!-- <text v-if="item.contract.status == -1">已作废</text>-->
</view>
<view v-if="item.order" class="text-main">
(订单状态:{{
item.order.orderStatus === 4 ? '已完成' : ([5, 6].includes(item.order.orderStatus) ? '订单终止' : '执行中')
}})
</view>
</view>
<view class="contract-left-center">
<view>
合同编码:{{ item.contract.transactionId }}
</view>
<view v-if="item.sheet && item.contract.type === 'old'">
交易编码:{{
item.contract.type === 'enquiry' ? item.sheet.enquiryNo : item.sheet.orderNo
}}
</view>
<view>
创建时间:{{ item.contract._add_time_str }}
</view>
<view v-if="item.contract.signEndDate">
签订截止时间:{{
dayjs(item.contract.signEndDate).format('YYYY-MM-DD HH:mm:ss')
}}
</view>
<view v-if="item.contract.type === 'sign' && item.contract.payUser && item.contract.payUser.user">
费用承担方:{{
item.contract.payUser.user.accountType === 2 ?
item.contract.payUser.user.company.companyName : item.contract.payUser.user.person.personName
}}
</view>
<template v-if="parseInt(item.contract.status) > 0">
<view v-for="(party, partyIndex) in item.contract.partyList.filter(p => p.role !== 'third')"
:key="`${partyIndex}_b`">
<template v-if="party.userId === item.contract.userId">
发起方签订完成时间:{{
party.signedAt ? dayjs(party.signedAt).format('YYYY-MM-DD HH:mm:ss') : ''
}}
</template>
<template v-if="party.userId !== item.contract.userId">
接收方签订完成时间:{{
party.signedAt ? dayjs(party.signedAt).format('YYYY-MM-DD HH:mm:ss') : ''
}}
</template>
<template v-if="party.role === 'third'">
第三方签订完成时间:{{
party.signedAt ? dayjs(party.signedAt).format('YYYY-MM-DD HH:mm:ss') : ''
}}
</template>
</view>
<view v-for="(party, partyIndex) in item.contract.partyList.filter(p => p.role === 'third')"
:key="partyIndex">
第{{ numToChForThird[partyIndex] }}方签订完成时间:{{
party.signedAt ? dayjs(party.signedAt).format('YYYY-MM-DD HH:mm:ss') : ''
}}
</view>
</template>
</view>
<view class="contract-left-bottom">
<el-button size="mini" plain @click.native="showParty(item)">
查看交易方
</el-button>
<el-button
v-if="item.contract.status === 0 && item.contract.userId === vk.getVuex('$user.userInfo._id')"
@click="createContract(item.contract)" size="mini" type="primary">创建合同
</el-button>
<el-button
v-if="item.contract.isMine"
@click="delContract(item.contract)" size="mini" type="text">
删除合同
</el-button>
<template v-if="parseInt(item.contract.status) === 1">
<el-button @click="extSign(item.sign, item.contract)" size="mini" type="primary"
:disabled="item.sign">
{{ item.sign ? '已签署' : '签署文件' }}
</el-button>
</template>
<template v-if="parseInt(item.contract.status) === 2">
<el-button
@click="viewContract(item.contract)"
size="mini" plain>
{{ parseInt(item.contract.signType) === 1 ? '查看单据' : '查看合同' }}
</el-button>
<el-button @click="downloadContract(item.contract.transactionId)"
type="primary"
size="mini" plain>
{{ parseInt(item.contract.signType) === 1 ? '下载单据' : '下载合同' }}
</el-button>
<el-button @click="payDownloadContract(item.contract)"
size="mini" plain>
{{
parseInt(item.contract.signType) === 1 ? '下载单据(付费司法调证)' : '下载合同(付费司法调证)'
}}
</el-button>
</template>
<el-button
v-if="item.order && parseInt(item.contract.status) === 2 && ((item.sheet && item.sheet.tradeMethod === 1) || item.contract.tradeMethod === 1)"
@click="navTo(`/pages/order/order?menuIndex=${item.role === 'buyer' ? 1 : 0}&keyword=${item.order.orderNo}`)"
size="mini"
:type="(item.order && item.order.orderStatus > 4) ? 'info' :'success'">
{{
(item.order && item.order.orderStatus === 4) ? '订单已结束' : ([5, 6].includes(item.order.orderStatus) ? '订单已终止' : (item.role === 'buyer' ? '去付款' : '去请款'))
}}
</el-button>
<el-button
v-if="item.contract.otherFile && item.contract.otherFile.length > 0"
@click="openDownload(item.contract.otherFile)" plain
size="mini"
type="info">查看附件
</el-button>
</view>
</view>
<view class="contract-right">
<!-- <view class="">
{{item._add_time_str}}到期,到期将无法签署合同
</view> -->
<view class="">
<el-button @click.native="changeContract(item.contract)"
v-if="((item.sheet && item.sheet.signMethod === 2) || item.contract.type === 'sign' || item.contract.type === 'self-support') && item.contract.status === 1
&& item.contract.userId === vk.getVuex('$user.userInfo._id') && item.contract.signCount === 0"
size="mini" type="primary">修改合同内容
</el-button>
</view>
</view>
</view>
<view class="paginatio">
<el-pagination @current-change="query" background layout="prev, pager, next"
:total="tableData.total"></el-pagination>
</view>
</view>
<view v-if="isSign" class="bg-white p-20">
<view class="flex justify-start items-center pb-30">
<text class="text-main mr-35">您剩余金久信签约宝次数: {{ userInfo.qybNum }}次</text>
<el-button size="mini" type="primary" @click.native="dialogVisible = true">立即购买
</el-button>
</view>
<view class="text-main mt-10">温馨提示:尊敬的用户如果您的剩余金久信签约宝次数为0次,请先进行购买,再进行签约。</view>
<vk-data-form ref="form2" v-model="form2.data" :action="form2.props.action"
:columns="form2.props.columns"
:rules="form2.props.rules" :form-type="form2.props.formType"
:loading.sync="form2.props.loading" :show-cancel="false"
:before-action="form2.props.beforeAction"
:auto-close="true" label-width="140px" @success="onFormSuccess">
<template v-slot:contractFile>
<vk-data-upload v-model="form2.data.contractFile" :cloudPathRemoveChinese="false" list-type="text"
accept=".pdf" :on-success="uploadSuccess" ref="uploader" :before-upload="beforeUpload"
:show-file-list="false">
</vk-data-upload>
<view class="text-main text-24 my-15 pointer" @click="openPreview(form2.data.contractFile)"
v-if="form2.data.contractFile">预览:
{{ form2.data.contractFile[form2.data.contractFile.length - 1].split('-').pop() }}
</view>
<view class="tips text-24 text-gray">
仅支持PDF文件,如有其他附件或多个单据签约请合并成一个PDF文档上传。且仅支持纵向文档上传,横向文档显示不完整。电子签署文件中需添加以下内容以增强双方约束力和文件法律效力:签约双方均自愿通过以“电子签约”形式进行签约,电子签名和加盖电子印章双方均予以认可具有法律效力。
</view>
</template>
<template v-slot:download>
<el-button size="mini" type="text" @click="downloadContractTemplate">点击下载
</el-button>
</template>
<template v-slot:tips>
<view class="tips text-24 text-gray">注意事项(电子签名不适用文件):</view>
<view class="tips text-24 text-gray">1.婚姻、继承、收养等人身关系的;</view>
<view class="tips text-24 text-gray">2.涉及停止供水、供热、供气等公用事业服务的;</view>
<view class="tips text-24 text-gray">3.其他法律、行政法规规定的不适用电子文书的其他形式;</view>
</template>
<template v-slot:signEndDate>
<view class="flex flex-col justify-start items-start">
<el-date-picker
:popper-class="'currentDatePickerClass'"
v-model="form2.data.signEndDate"
type="datetime"
placeholder="选择签署截止日期">
</el-date-picker>
<view class="text-25 text-gray mt-15">请合理规划预留各签约方时间避免合同过时失效,失效需重新发起。
</view>
</view>
</template>
<template v-slot:third>
<div v-for="(item, index) in form2.data.thirdList" :key="`${index}_c`"
class="flex justify-start items-center mb-10 ">
<span>第{{ numToChForThird[index] }}方</span>
<div class="mx-15">
<el-input v-model="form2.data.thirdList[index].phone"
placeholder="请输入第三方手机号"/>
</div>
<div>
<el-input v-model="form2.data.thirdList[index].signKeyword"
placeholder="请输入第三方签章关键字"/>
</div>
<div class="flex justify-start items-center">
<i class="el-icon-circle-plus pointer text-main text-50 ml-35"
@click="addThird"/>
<i class="el-icon-remove pointer text-50 ml-35"
@click="removeThird(index)"/>
<i title="上移" class="el-icon-arrow-up pointer text-50 ml-35" v-if="index > 0"
@click="thirdMoveUp(index)"/>
<i title="下移" class="el-icon-arrow-down pointer text-50 ml-35"
@click="thirdMoveDown(index)"
v-if="index < form2.data.thirdList.length - 1"/>
</div>
</div>
<p class="text-25 text-gray mt-20">如无第三方签约可忽略,签章关键字使用方法同下。</p>
<p class="text-25 text-gray mt-20">手机号请连续输入,请勿添加空格和断开输入,系统无法准确识别。</p>
</template>
<template v-slot:footer="{ loading }">
<view style="text-align: center;">
<el-button :loading="loading" type="primary" @click="form2.props.action">提交</el-button>
</view>
</template>
</vk-data-form>
</view>
<view v-if="isSignIntro" class="bg-white p-20">
<div v-html="signIntro"></div>
</view>
<view v-if="isSignFlow" class="bg-white p-20">
<div v-html="signFlow"></div>
</view>
<view v-if="isSignTips" class="bg-white p-20">
<div v-html="signTips"></div>
</view>
<!-- AI 客服(嵌入式) -->
<view v-if="menuIndex === '3'" class="ai-chat-embed">
<view class="chat-header-bar">
<text class="chat-title">🤖 金久信签约宝 AI 智能客服</text>
</view>
<scroll-view scroll-y class="chat-msg-list" :scroll-into-view="chatScrollId"
scroll-with-animation>
<view v-if="chatMessages.length === 0" class="chat-row left" id="chat-welcome">
<view class="chat-avatar">🤖</view>
<view class="chat-bubble">
您好!我是金久信签约宝AI智能客服,可以帮您解答金久信签约宝使用过程中注册、操作、适用场景等问题。请问有什么可以帮您?
<view class="quick-btns">
<view class="quick-btn" @click="chatQuickSend('注册准备资料事项')">注册准备资料事项</view>
<view class="quick-btn" @click="chatQuickSend('关键字无法识别')">关键字无法识别</view>
<view class="quick-btn" @click="chatQuickSend('金久信签约宝法律效力')">金久信签约宝法律效力</view>
<view class="quick-btn" @click="chatQuickSend('电子签约不适用场景')">电子签约不适用场景</view>
</view>
</view>
</view>
<view v-for="(msg, i) in chatMessages" :key="i" :id="'chat-msg' + i"
:class="['chat-row', msg.role === 'user' ? 'right' : 'left']">
<view v-if="msg.role !== 'user'" class="chat-avatar">🤖</view>
<view class="chat-bubble" v-html="formatChatContent(msg.content)"></view>
</view>
<view v-if="chatLoading" class="chat-row left" id="chat-loading">
<view class="chat-avatar">🤖</view>
<view class="chat-bubble typing">思考中...</view>
</view>
</scroll-view>
<view class="chat-input-bar">
<input v-model="chatInput" @confirm="chatSend" placeholder="输入您的问题..." />
<view class="chat-send-btn" @click="chatSend">发送</view>
</view>
</view>
<!-- 合同审查 -->
<view v-if="menuIndex === '4'" class="review-container">
<view class="review-title">📋 AI 合同审查</view>
<!-- 积分提示 -->
<view class="points-notice">
<span class="pn-icon">💎</span>
<view class="pn-text">
<view class="pn-title">合同审查消耗积分</view>
<view class="pn-desc">根据文件大小自动计算,每 100KB 约 200 积分。审查后余额需保留至少 200 积分。</view>
</view>
<view class="pn-balance">余额:{{ pointsBalance }} 分</view>
</view>
<!-- 免责声明 -->
<view class="disclaimer-notice">
<span class="dn-icon">⚠️</span>
<view class="dn-text">请确保合同内容清晰可见。如因合同不清晰导致分析失败,平台不承担后果,积分正常消耗。</view>
</view>
<!-- 上传区 -->
<view v-if="!reviewFile" class="upload-area" @click="selectReviewFile">
<view class="upload-icon">📤</view>
<view class="upload-title">上传合同文件</view>
<view class="upload-desc">仅支持 PDF / Word(.pdf .doc .docx),最大 20MB</view>
<view class="upload-btn">选择文件上传</view>
</view>
<!-- 上传中提示 -->
<view v-if="reviewUploading" style="text-align:center;padding:30px 0;">
<i class="el-icon-loading" style="font-size:32px;color:#a92924;"></i>
<p style="font-size:14px;color:#666;margin-top:8px;">正在上传文件到审查系统...</p>
</view>
<!-- 文件信息 + 审查选项 -->
<view v-if="reviewFile && !reviewResult && !reviewUploading">
<view class="file-info">
<span class="fi-icon">📄</span>
<view>
<view class="fi-name">{{ reviewFile.name }}</view>
<view class="fi-size">{{ formatFileSize(reviewFile.size) }}</view>
</view>
<span class="fi-remove" @click="reviewFile = null">✕</span>
</view>
<view class="review-opts">
<view :class="['review-opt', reviewMode === 0 && 'selected']" @click="reviewMode = 0">
<view class="opt-icon">🔍</view>
<view class="opt-title">标准审查</view>
<view class="opt-desc">风险条款识别 · 合规性检查</view>
<!-- <view class="opt-points">{{ reviewPoints }} 积分</view> -->
</view>
<!-- <view :class="['review-opt', reviewMode === 1 && 'selected']" @click="reviewMode = 1">
<view class="opt-icon">📊</view>
<view class="opt-title">深度分析</view>
<view class="opt-desc">标准审查 + 估价参考 + 修改建议</view>
<view class="opt-points">{{ reviewPoints * 2 }} 积分</view>
</view> -->
</view>
<view style="text-align:center;margin-top:16px;">
<el-button type="primary" :loading="reviewLoading"
:disabled="pointsBalance < reviewPoints + 200"
@click="startReview">
{{ reviewLoading ? '审查中...' : '开始审查' }}
</el-button>
<p v-if="pointsBalance < reviewPoints + 200" style="font-size:12px;color:#f56c6c;margin-top:6px;">
⚠️ 积分不足,当前余额 {{ pointsBalance }},需要 {{ reviewPoints }} 积分且审查后余额需保留至少 200 积分。
<span style="color:#a92924;cursor:pointer;text-decoration:underline;" @click="goRecharge">去充值</span>
</p>
</view>
</view>
<!-- 审查中 -->
<view v-if="reviewLoading" style="text-align:center;padding:40px 0;">
<div style="font-size:48px;margin-bottom:16px;">🤖</div>
<div style="font-size:16px;font-weight:600;margin-bottom:8px;">AI 正在分析合同...</div>
<el-progress :percentage="reviewProgress" :stroke-width="8"
style="max-width:400px;margin:0 auto;"></el-progress>
</view>
<!-- 审查结果 -->
<view v-if="reviewResult">
<view class="result-header">
<h3>📋 {{ reviewStreaming ? 'AI 正在分析...' : '审查结果' }} — {{ reviewFile.name }}</h3>
<span class="result-status">{{ reviewStreaming ? '⏳ 分析中...' : '✅ 审查完成' }}</span>
</view>
<view class="result-body" :style="reviewStreaming ? 'max-height:400px;overflow-y:auto;' : ''">
<div v-html="reviewResult"></div>
<div v-if="reviewStreaming" style="margin-top:8px;">
<span style="display:inline-block;width:8px;height:8px;background:#a92924;border-radius:50%;animation:pulse 1s infinite;"></span>
<span style="font-size:12px;color:#999;margin-left:6px;">AI 正在输出分析结果...</span>
</div>
</view>
<view class="result-footer">
<el-button v-if="reviewHtmlUrl" type="success" size="small" @click="openUrl(reviewHtmlUrl)">📄 查看完整 HTML 报告</el-button>
<el-button size="small" @click="resetReview">重新审查</el-button>
</view>
</view>
</view>
<!-- 我的报告 -->
<view v-if="menuIndex === '5'" class="review-container">
<view class="review-title">
📈 我的报告
<el-button size="mini" type="primary" style="margin-left:12px;" @click="queryReports()">🔍 查询报告</el-button>
</view>
<view v-if="reportList.length === 0" style="text-align:center;padding:60px 0;color:#999;">
<div style="font-size:48px;margin-bottom:12px;">📋</div>
暂无审查报告,点击上方「查询报告」从服务器加载
</view>
<view v-for="(r, i) in reportList" :key="i" class="report-item">
<view class="report-info">
<view class="report-name">📄 {{ r.fileName }}</view>
<view class="report-time">{{ r.time }} · 消耗 {{ Math.abs(r.amount) }} 积分</view>
</view>
<el-tag v-if="r.status === 'uploaded'" size="mini" type="info">待审查</el-tag>
<el-tag v-else-if="r.status === 'reviewing'" size="mini" type="warning">排队中</el-tag>
<el-tag v-else-if="r.status === 'processing'" size="mini" type="warning">审查中</el-tag>
<el-tag v-else-if="r.status === 'completed'" size="mini" type="success">已完成</el-tag>
<el-tag v-else-if="r.status === 'failed'" size="mini" type="danger">失败</el-tag>
<el-button v-if="r.htmlUrl" size="mini" type="text" @click="openUrl(r.htmlUrl)">查看报告</el-button>
<el-button v-if="r.fileUrl" size="mini" type="text" @click="previewFile(r)">查看文件</el-button>
<el-button v-if="r.status === 'completed'" size="mini" type="text" @click="viewReport(r)">查看摘要</el-button>
</view>
</view>
<!-- 积分管理 -->
<view v-if="menuIndex === '6'" class="review-container">
<view class="review-title">💎 积分管理</view>
<view class="points-summary">
<view class="ps-card">
<view class="ps-label">可用积分</view>
<view class="ps-value">{{ pointsBalance }}</view>
</view>
<view class="ps-card secondary">
<view class="ps-label">累计获得</view>
<view class="ps-value">{{ pointsTotalEarned }}</view>
</view>
</view>
<!-- 积分充值 -->
<view class="review-title" style="font-size:14px;margin-top:16px;">💰 积分充值</view>
<view class="recharge-grid">
<view v-for="(pkg, i) in rechargePackages" :key="i"
:class="['recharge-card', selectedRecharge === i && 'selected']"
@click="selectedRecharge = i">
<view class="rc-points">{{ pkg.points }}</view>
<view class="rc-price">{{ pkg.price }}</view>
<view v-if="pkg.bonus" class="rc-bonus">{{ pkg.bonus }}</view>
</view>
</view>
<view style="text-align:center;margin-bottom:24px;">
<el-button type="primary" size="medium" @click="doRecharge">
立即充值 {{ rechargePackages[selectedRecharge].price }}
({{ rechargePackages[selectedRecharge].points }} 积分)
</el-button>
<p style="font-size:11px;color:#999;margin-top:6px;">💡 积分长期有效,可用于合同审查等功能</p>
</view>
<!-- 使用说明 -->
<view class="review-title" style="font-size:14px;margin-top:16px;">📖 积分使用说明</view>
<view style="font-size:13px;color:#666;line-height:2;margin-bottom:20px;">
<p>• <strong>合同审查</strong>:每 100KB 约 200 积分(根据文件大小自动计算)</p>
<p>• <strong>AI 客服</strong>:免费 — 签约宝专属问答</p>
<p>• 新用户注册自动赠送 500 体验积分</p>
<p>• 积分长期有效,不过期</p>
</view>
<view class="review-title" style="font-size:14px;">
📜 积分明细
<el-button size="mini" type="text" style="margin-left:12px;" @click="getPointsLog()">🔍 查看</el-button>
</view>
<el-table :data="pointsLogList" style="width:100%">
<el-table-column prop="_add_time_str" label="时间" width="180"></el-table-column>
<el-table-column label="类型" width="80">
<template slot-scope="scope">
<el-tag size="mini" :type="scope.row.type === 'earn' ? 'success' : 'danger'">
{{ scope.row.type === 'earn' ? '获得' : '消耗' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="reason" label="原因"></el-table-column>
<el-table-column label="变动" width="100">
<template slot-scope="scope">
<span :style="{color: scope.row.type === 'earn' ? '#67c23a' : '#f56c6c'}">
{{ scope.row.type === 'earn' ? '+' : '' }}{{ scope.row.amount }}
</span>
</template>
</el-table-column>
<el-table-column prop="balance_after" label="余额" width="100"></el-table-column>
</el-table>
</view>
</el-col>
</el-row>
</view>
<common-footer></common-footer>
<el-dialog :visible.sync="form1.props.show" width="70%">
<vk-data-form ref="form1" v-model="form1.data" :action="form1.props.action" :columns="form1.props.columns"
:rules="form1.props.rules" :form-type="form1.props.formType"
:loading.sync="form1.props.loading" :show-cancel="false"
:auto-close="true" label-width="140px" @success="onFormSuccess">
<template v-slot:download>
<el-button size="mini" type="text" @click="downloadContractTemplate">点击下载</el-button>
</template>
<template v-slot:signEndDate>
<view class="flex flex-col justify-start items-start">
<el-date-picker
:popper-class="'currentDatePickerClass'"
v-model="form1.data.signEndDate"
type="datetime"
placeholder="选择签署截止日期">
</el-date-picker>
<view class="text-25 text-gray mt-15">请合理规划预留各签约方时间避免合同过时失效,失效需重新发起。</view>
</view>
</template>
<template v-slot:contractFile>
<vk-data-upload v-model="form1.data.contractFile" :cloudPathRemoveChinese="false" list-type="text"
accept=".pdf" :on-success="uploadSuccess1" ref="uploader1" :before-upload="beforeUpload"
:show-file-list="false">
</vk-data-upload>
<view class="text-main text-24 my-15 pointer" @click="openPreview(form1.data.contractFile)"
v-if="form1.data.contractFile">预览:
{{ form1.data.contractFile[form1.data.contractFile.length - 1].split('-').pop() }}
</view>
<view class="tips text-24 text-gray">
仅支持PDF文件,如有其他附件或多个单据签约请合并成一个PDF文档上传。且仅支持纵向文档上传,横向文档显示不完整。电子签署文件中需添加以下内容以增强双方约束力和文件法律效力:签约双方均自愿通过以“电子签约”形式进行签约,电子签名和加盖电子印章双方均予以认可具有法律效力。
</view>
</template>
<template v-slot:third>
<div v-for="(item, index) in form1.data.thirdList" :key="`${index}_d`"
class="flex justify-start items-center mb-10 ">
<span>第{{ numToChForThird[index] }}方</span>
<div class="mx-15">
<el-input v-model="form1.data.thirdList[index].phone"
placeholder="请输入第三方手机号"/>
</div>
<div>
<el-input v-model="form1.data.thirdList[index].signKeyword"
placeholder="请输入第三方签章关键字"/>
</div>
<div class="flex justify-start items-center">
<i class="el-icon-circle-plus pointer text-main text-50 ml-35"
@click="addThird1"/>
<i class="el-icon-remove pointer text-50 ml-35"
@click="removeThird1(index)"/>
<i title="上移" class="el-icon-arrow-up pointer text-50 ml-35" v-if="index > 0"
@click="thirdMoveUp1(index)"/>
<i title="下移" class="el-icon-arrow-down pointer text-50 ml-35"
@click="thirdMoveDown1(index)"
v-if="index < form1.data.thirdList.length - 1"/>
</div>
</div>
<p class="text-25 text-gray mt-20">手机号请连续输入,请勿添加空格和断开输入,系统无法准确识别</p>
<p class="text-25 text-gray mt-20">如无第三方签约可忽略,签章关键字使用方法同下。</p>
</template>
<template v-slot:footer="{ loading }">
<view style="text-align: center;">
<el-button :loading="loading" type="primary" @click="form1Action">提交</el-button>
</view>
</template>
</vk-data-form>
</el-dialog>
<el-dialog class="qyb-buy-dialog" title="金久信签约宝次数" :visible.sync="dialogVisible" width="50%">
<p class="qyb-buy-title">请选择购买次数</p>
<div class="qyb-package-grid">
<div class="qyb-package-card pointer"
:class="{ 'buy-quote-mode-active': selectedQyb === item._id }"
v-for="(item, index) in qybPackageList" :key="`${index}_e`"
@click="selectedQyb = item._id">
<p class="qyb-package-amount">{{ item.amount }}元</p>
<p class="qyb-package-times">可获得{{ item.getNum }}次</p>
</div>
</div>
<div class="qyb-remain-num text-main font-bold">
剩余金久信签约宝次数:{{ userInfo.qybNum }}次
</div>
<div class="qyb-dialog-footer flex justify-center items-center">
<el-button type="primary" :disabled="!selectedQyb" @click.native="doBuyQyb">确定</el-button>
<el-button plain @click.native="dialogVisible = false">取消</el-button>
</div>
</el-dialog>
<el-dialog title="申请发票" :visible.sync="showInvoice" width="50%">
<p>请输入开票信息</p>
<el-form ref="form" label-width="140px">
<el-form-item label="名称" required>
<el-input v-model="invoiceForm.name" placeholder="请输入企业名称或个人姓名"></el-input>
</el-form-item>
<el-form-item label="税号/证号" required>
<el-input v-model="invoiceForm.code" placeholder="请输入税号或身份证号"></el-input>
</el-form-item>
<el-form-item label="手机号" required>
<el-input v-model="invoiceForm.phone" placeholder="请输入手机号"></el-input>
</el-form-item>
<el-form-item label="电子邮箱" required>
<el-input v-model="invoiceForm.email" placeholder="请输入电子邮箱"></el-input>
</el-form-item>
<el-form-item label="开户行">
<el-input v-model="invoiceForm.bankName" placeholder="请输入开户行"></el-input>
</el-form-item>
<el-form-item label="银行账号">
<el-input v-model="invoiceForm.bankAccount" placeholder="请输入银行账号"></el-input>
</el-form-item>
<el-form-item label="地址" required>
<el-input v-model="invoiceForm.address" placeholder="请输入地址"></el-input>
</el-form-item>
<el-form-item label="单位/个人电话">
<el-input v-model="invoiceForm.tel" placeholder="请输入单位/个人电话"></el-input>
</el-form-item>
</el-form>
<div class="mt-20 flex justify-center items-center">
<el-button type="primary" @click.native="doInvoice">确定</el-button>
<el-button plain @click.native="showInvoice = false">取消</el-button>
</div>
</el-dialog>
<el-dialog title="支付凭证" :visible.sync="showPayVoucher" width="50%">
<el-image :src="payVoucher" :preview-src-list="[payVoucher]"/>
</el-dialog>
</view>
</template>
<script>
import {Agreement} from "@icon-park/vue";
import dayjs from "dayjs";
import {Loading} from "element-ui";
export default {
components: {Agreement},
data() {
return {
numToChForThird: ['三', '四', '五', '六', '七', '八', '九', '十'],
userInfo: {
qybNum: 0
},
dialogVisible: false,
qybPackageList: [],
selectedQyb: null,
contractTemplate: '',
pageIndex: 1,
menuIndex: '0',
isSign: false,
isSignIntro: false,
isSignFlow: false,
isSignLog: false,
signIntro: '',
signFlow: '',
signFlowTitle: '',
signType: '1',
signTypeFilter: '2',
keyword: '',
formData: {
status: 0,
tradeMethod: null,
type: null
},
showPayVoucher: false,
payVoucher: '',
signLog: {},
signBuyLog: {},
signLogType: 'log',
showInvoice: false,
isSignTips: false,
signTips: '',
// AI 客服数据
chatMessages: [],
chatInput: '',
chatLoading: false,
chatConversationId: null,
chatScrollId: '',
// 积分数据
pointsBalance: 0,
pointsTotalEarned: 0,
pointsLogList: [],
// 合同审查数据
reviewFile: null,
reviewFileId: null,
reviewRecordId: null,
reviewHtmlUrl: '',
reviewPdfUrl: '',
reviewContractInfo: {},
reviewStreaming: false,
reviewUploading: false,
reviewMode: 0,
reviewPoints: 100,
reviewLoading: false,
reviewProgress: 0,
reviewResult: null,
reviewConversationId: null,
// 积分充值
selectedRecharge: 1,
rechargePackages: [{
points: 10000,
price: '20元',
bonus: ''
},
{
points: 50000,
price: '100元',
bonus: ''
},
{
points: 100000,
price: '200元',
bonus: ''
},
{
points: 450000,
price: '900元',
bonus: ''
},
{
points: 1000000,
price: '2000元',
bonus: ''
},
{
points: 2000000,
price: '4000元',
bonus: ''
}
],
// 我的报告
reportList: [],
invoiceForm: {
id: null,
name: null,
code: null,
phone: null,
email: null,
bankName: null,
bankAccount: null,
address: null,
tel: null,
},
tableData: {},
// 表单相关开始-----------------------------------------------------------
form1: {
// 表单请求数据,此处可以设置默认值
data: {
contractId: '',
signEndDate: '',
title: '',
contractFile: '',
buyerSignKeyword: '',
sellerSignKeyword: '',
otherFile: '',
thirdList: [{phone: '', signKeyword: ''}],
},
// 表单属性
props: {
// 表单请求地址
action: async () => {
},
// 表单字段显示规则
columns: [
{
key: "title",
title: "标题",
type: "text"
},
{
key: "third",
title: "",
type: "html",
tips: "如无第三方签约可忽略,签章关键字使用方法同下。"
},
{
key: "signEndDate",
title: "签署截止日期",
type: "date",
tips: '发起方填写签署截止日期和时间即为各签约方共同认可的签署时间,请酌情填写仔细阅读签署文件。',
dateType: "datetime",
pickerOptions: {
disabledDate: (date) => {
const now = new Date()
return now > date
}
}
},
{
key: "contractFile",
title: "签署文档",
type: "file",
limit: 1,
accept: ".pdf",
tips: "仅支持PDF文件,如有其他附件或多个单据签约请合并成一个PDF文档上传。且仅支持纵向文档上传,横向文档显示不完整。"
},
// {
// key: "download",
// title: "合同模板下载",
// type: "html"
// },
{
key: "buyerSignKeyword",
title: "签约发起方关键字",
type: "text",
tips: '先点击上传文档后,在线预览文档,点击ctrl+f,复制粘贴或输入预盖章部位关键字至弹框内,再次点击ctrl+f进行核验确认位置和内容,核实无误后可返回签署页面输入测验关键字后进行签署(如同一关键字文档多次使用无误后可考虑省略检测流程)。系统将按此关键字进行签章位置的定位,将电子章盖在这个关键字上面。凡出现关键字的地方均会盖上指定用户的电子章,建议关键字在合同中保持唯一。',
placeholder: '发起方盖章关键字',
},
{
key: "sellerSignKeyword",
title: "签约接收方关键字",
type: "text",
placeholder: '签约接收方关键字',
tips: '先点击上传文档后,在线预览文档,点击ctrl+f,复制粘贴或输入预盖章部位关键字至弹框内,再次点击ctrl+f进行核验确认位置和内容,核实无误后可返回签署页面输入测验关键字后进行签署(如同一关键字文档多次使用无误后可考虑省略检测流程)。系统将按此关键字进行签章位置的定位,将电子章盖在这个关键字上面。凡出现关键字的地方均会盖上指定用户的电子章,建议关键字在合同中保持唯一。'
},
// {
// key: "otherFile",
// title: "附件",
// type: "file",
// accept: ".pdf",
// tips: "仅支持PDF文件(附件将按顺序附在合同末尾一并签章)",
// },
],
// 表单验证规则
rules: {
title: [{
required: true,
message: "标题不能为空",
trigger: "change"
},],
signEndDate: [{
required: true,
message: "签署截止日期不能为空",
trigger: "change"
}],
buyerSignKeyword: [{
required: true,
message: "签约发起方关键字不能为空",
trigger: "change"
}],
sellerSignKeyword: [{
required: true,
message: "签约接收方关键字不能为空",
trigger: "change"
}],
contractFile: [{
required: true,
message: "签署文档不能为空",
trigger: "change"
},]
},
// add 代表添加 update 代表修改
formType: "",
// 表单是否在请求中
loading: false,
// 是否显示表单1 的弹窗
show: false
}
},
form2: {
// 表单请求数据,此处可以设置默认值
data: {
contractId: '',
signEndDate: '',
title: '',
contractFile: '',
buyerSignKeyword: '',
sellerSignKeyword: '',
signType: '1',
tradeMethod: 0,
payBy: 0,
otherFile: [],
thirdList: [{phone: '', signKeyword: ''}],
},
// 表单属性
props: {
// 表单请求地址
action: async () => {
const thirdKeywords = []
for (let i = 0; i < this.form2.data.thirdList.length; i++) {
const item = this.form2.data.thirdList[i]
if (item.phone && item.signKeyword) {
thirdKeywords.push(item.signKeyword)
}
}
uni.showLoading({title: '查验中'})
vk.request({
method: "POST",
url: 'https://hcsc.gxwebsoft.com/api/read-pdf',
header: {
"content-type": "application/json; charset=utf-8",
},
data: {
file: this.form2.data.contractFile[this.form2.data.contractFile.length - 1],
buyerSignKeyword: this.form2.data.buyerSignKeyword,
sellerSignKeyword: this.form2.data.sellerSignKeyword,
thirdKeywords
},
success: async (pdfRes) => {
},
fail: (pdfRes) => {
uni.hideLoading()
this.$message.error(pdfRes.content.msg)
},
complete: async (pdfRes) => {
if (pdfRes.code !== 200) this.$message.error(pdfRes.content.msg)
else {
uni.hideLoading()
this.form2.props.loading = true
this.form2.data.thirdList = this.form2.data.thirdList.filter(item => item.phone && item.signKeyword)
if (this.form2.data.thirdList.length > 0) {
for (let i = 0; i < this.form2.data.thirdList.length; i++) {
const item = this.form2.data.thirdList[i]
if (item.phone === '') {
vk.toast('请填写手机号')
this.form2.props.loading = false
return false
}
if (item.signKeyword === '') {
vk.toast('请填写签章关键字')
this.form2.props.loading = false
return false
}
if (item.phone === this.form2.data.phone) {
vk.toast('第三方手机号不能与接收方手机号相同')
this.form2.props.loading = false
return false
}
}
}
if (this.form2.data.payBy === undefined || this.form2.data.payBy === '' || this.form2.data.payBy === null) {
vk.toast('请选择费用承担方')
this.form2.props.loading = false
return false
}
if (!this.form2.data.signEndDate) {
vk.toast('请选择签署截止时间')
this.form2.props.loading = false
return false
}
if (dayjs(this.form2.data.signEndDate).isBefore(dayjs())) {
vk.toast('签署截止日期不能早于当前时间')
this.form2.props.loading = false
return false
}
const res = await this.$confirm(`1、您确认,若全体各签约方未完成全部签约流程,本签约文件不发生法律效力,系统无法对该签约文件予以保存及提供司法调证服务;<br>2、您确认,当前签约的实名主体名称、手机号码与注册认证阶段提交的信息未发生变更;若发生变更,必须及时进行重新认证后方可继续签约,避免产生严重法律风险与经济损失;<br>3、您确认,本次签约为本系统实名认证主体操作,如若不是实名认证主体操作,授权委托人需本系统进行授权认证后签约;<br>4、您确认,您与其他签约方均系在平等、自愿的基础上完成本次签约;<br>5、你确认,此次签约内容符合《中华人民共和国电子签名法》《民法典》生效保护范畴;<br>6、如因法律法规、监管部门政策更新等原因,系统提示您重新进行实名认证、证书续期等重要提示时,请您必须在签约前完成,否则造成任何损失,平台不承担任何责任;以上签约条件您均已经知悉并确认,否则您有权拒绝本次签约。`, '特别重要提示', {
confirmButtonText: '知悉同意',
showCancelButton: false,
type: 'warning',
dangerouslyUseHTMLString: true
})
if (res === 'confirm') {
await vk.callFunction({
url: 'client/contract/kh/addSign',
data: this.form2.data
})
vk.redirectTo({url: '/pages/contract/index?tab=1'})
}
}
console.log('complete:', pdfRes)
uni.hideLoading()
},
})
},
// 表单字段显示规则
columns: [
{
key: "signType",
title: "",
type: "radio",
data: [
{
value: '1',
label: "单据签约"
},
{
value: '2',
label: "合同签约"
}
]
},
{
key: "tradeMethod",
title: "交易方式",
type: "radio",
data: [
{
value: 0,
label: "自主交易"
},
{
value: 1,
label: "金久信公正"
}
],
showRule: (data) => {
return data.signType === '2';
}
},
{
key: "payBy",
title: "签约费用",
type: "radio",
data: [
{
value: 0,
label: "发起方承担"
},
{
value: 1,
label: "接收方承担"
},
{
value: 2,
label: "第三方承担"
},
]
},
// {
// key: "payByThird",
// title: "承担签约费用的第三方手机号",
// type: "text",
// showRule: (data) => {
// return data.payBy === 2;
// }
// },
{
key: "title",
title: "标题",
placeholder: `请输入自定义合同或单据名称`,
type: "text"
},
{
key: "phone",
title: "对方手机号",
type: "text",
placeholder: "请输入对方金久信实名认证手机号码",
tips: "手机号请连续输入,请勿添加空格和断开输入,系统无法准确识别"
},
{
key: "userType",
title: "对方身份",
type: "select",
data: [
{
value: 1,
label: "买家"
},
{
value: 2,
label: "卖家"
},
{
value: 3,
label: "甲方"
},
{
value: 4,
label: "乙方"
},
{
value: 5,
label: "授权签约方"
},
]
},
{
key: "signEndDate",
title: "签署截止时间",
tips: '发起方填写签署截止日期和时间即为各签约方共同认可的签署时间,请酌情填写仔细阅读签署文件。',
type: "date",
dateType: "datetime",
pickerOptions: {
disabledDate: (date) => {
const now = new Date()
return now > date
}
}
},
{
key: "contractFile",
title: "签署文档",
type: "file",
limit: 1,
accept: ".pdf",
tips: "仅支持PDF文件,如有其他附件或多个单据签约请合并成一个PDF文档上传。且仅支持纵向文档上传,横向文档显示不完整。电子签署文件中需添加以下内容以增强双方约束力和文件法律效力:签约双方均自愿通过以“电子签约”形式进行签约,电子签名和加盖电子印章双方均予以认可具有法律效力。",
},
// {
// key: "download",
// title: "单据签约模板下载",
// type: "html",
// showRule: () => {
// return this.signType === '1'
// }
// },
{
key: "buyerSignKeyword",
title: "签约发起方关键字",
type: "text",
placeholder: '发起方盖章关键字',
tips: '先点击上传文档后,在线预览文档,点击ctrl+f,复制粘贴或输入预盖章部位关键字至弹框内,再次点击ctrl+f进行核验确认位置和内容,核实无误后可返回签署页面输入测验关键字后进行签署(如同一关键字文档多次使用无误后可考虑省略检测流程)。系统将按此关键字进行签章位置的定位,将电子章盖在这个关键字上面。凡出现关键字的地方均会盖上指定用户的电子章,建议关键字在合同中保持唯一。'
},
{
key: "sellerSignKeyword",
title: "签约接收方关键字",
type: "text",
placeholder: '签约接收方关键字',
tips: '先点击上传文档后,在线预览文档,点击ctrl+f,复制粘贴或输入预盖章部位关键字至弹框内,再次点击ctrl+f进行核验确认位置和内容,核实无误后可返回签署页面输入测验关键字后进行签署(如同一关键字文档多次使用无误后可考虑省略检测流程)。系统将按此关键字进行签章位置的定位,将电子章盖在这个关键字上面。凡出现关键字的地方均会盖上指定用户的电子章,建议关键字在合同中保持唯一。'
},
{
key: "third",
title: "",
type: "html"
},
{
key: "otherFile",
title: "合同附件",
type: "file",
showRule: () => {
return this.signType === '2'
}
},
{
key: "tips",
title: "",
type: "text",
placeholder: '',
},
],
// 表单验证规则
rules: {
title: [{
required: true,
message: "标题不能为空",
trigger: "change"
},],
phone: [{
required: true,
message: "请输入对方金久信实名注册手机号码 不能为空",
trigger: "change"
},],
signEndDate: [{
required: true,
message: "签署截止日期不能为空",
trigger: "change"
}],
buyerSignKeyword: [{
required: true,
message: "签约发起方关键字不能为空",
trigger: "change"
}],
sellerSignKeyword: [{
required: true,
message: "签约接收方关键字不能为空",
trigger: "change"
}],
contractFile: [{
required: true,
message: "签署文档不能为空",
trigger: "change"
},],
payBy: [{
required: true,
message: "请选择签约承担方",
trigger: "change"
},]
},
// add 代表添加 update 代表修改
formType: "",
// 表单是否在请求中
loading: false,
// 是否显示表单1 的弹窗
show: false
},
},
// 表单相关结束-----------------------------------------------------------
}
},
onLoad(options) {
if (!vk.getVuex('$user.userInfo.mobile')) {
this.$confirm('请先绑定手机号', '提示', {
confirmButtonText: '确定',
showCancelButton: false,
type: 'warning'
}).then(() => {
vk.redirectTo(`/pages/auth/login/wechat?state=jjx_` + vk.getVuex('$user.userInfo.accountType'))
}).catch(() => {
vk.redirectTo(`/pages/auth/login/wechat?state=jjx_` + vk.getVuex('$user.userInfo.accountType'))
})
return
}
if (!vk.getVuex('$user.userInfo.fadadaConfirm')) {
this.$confirm('您尚未认证, 请前往个人中心认证', '提示', {
confirmButtonText: '确定',
showCancelButton: false,
type: 'warning',
showClose: false,
closeOnClickModal: false,
closeOnPressEscape: false
}).then(() => {
vk.navigateTo({url: '/pages/user/user?menu=fadada'});
}).catch(() => {
vk.navigateTo({url: '/pages/user/user?menu=fadada'});
})
}
if (options.tab) {
if (options.tab === 'sign') {
// this.menuIndex = null
// this.formData.tradeMethod = null
// this.isSign = true
// this.isSignIntro = false
// this.isSignFlow = false
// this.isSignLog = false
// this.isSignTips = false
this.selectSign()
} else if (options.tab === 'signPayLog') {
this.getSignBuyLog(1)
} else {
this.menuIndex = String(options.tab)
if (options.tradeMethod) this.formData.tradeMethod = (parseInt(options.tradeMethod))
this.formData.status = Number(options.tab)
this.isSign = false
if (options.result_desc) {
uni.showLoading({title: '请求中...'})
setTimeout(async () => {
await this.query(1)
uni.hideLoading()
}, 1500)
} else this.query(1)
}
} else {
this.menuIndex = '0'
this.formData.status = 0
this.isSign = false
this.query(1)
}
this.getCommonContractUrl()
this.getQybPackage()
this.getSignFlow()
},
onShow() {
this.userInfo = vk.getVuex('$user.userInfo')
this.getUserInfo()
this.getPointsBalance()
if (this.menuIndex === '6') this.getPointsLog();
if (this.menuIndex === '5') this.getReportList();
},
methods: {
dayjs,
menuSelect(e) {
this.menuIndex = String(e)
this.formData.status = Number(e)
this.isSign = false
this.isSignIntro = false
this.isSignLog = false
this.isSignFlow = false
this.isSignTips = false
// AI 智能服务菜单(3/4/5/6)不调 query,加载对应数据
if (e >= 3) {
if (e === 4 || e === 6) this.getPointsBalance();
return;
}
this.query(1)
},
selectSign() {
this.isSign = true
this.isSignIntro = false
this.isSignFlow = false
this.isSignLog = false
this.isSignTips = false
this.menuIndex = ''
},
selectSignLog() {
this.isSign = false
this.isSignIntro = false
this.isSignFlow = false
this.isSignLog = true
this.isSignTips = false
this.menuIndex = ''
this.getSignLog(1)
},
async selectSignFlow() {
this.isSign = false
this.isSignIntro = false
this.isSignLog = false
this.isSignFlow = true
this.isSignTips = false
this.menuIndex = ''
},
// AI 客服方法
chatQuickSend(text) {
this.chatInput = text;
this.chatSend();
},
async chatSend() {
const msg = this.chatInput.trim();
if (!msg || this.chatLoading) return;
this.chatInput = '';
this.chatMessages.push({
role: 'user',
content: msg
});
this.chatLoading = true;
this.chatScrollToBottom();
try {
const res = await vk.callFunction({
url: 'client/ai/pub/chat',
data: {
message: msg,
bot_id: '7683509749475819554'
}
});
if (res.data.conversation_id) this.chatConversationId = res.data.conversation_id;
this.chatMessages.push({
role: 'assistant',
content: res.data.answer
});
} catch (e) {
this.chatMessages.push({
role: 'assistant',
content: '网络异常,请稍后重试'
});
}
this.chatLoading = false;
this.chatScrollToBottom();
},
chatScrollToBottom() {
setTimeout(() => {
if (this.chatMessages.length === 0) {
this.chatScrollId = 'chat-welcome';
} else if (this.chatLoading) {
this.chatScrollId = 'chat-loading';
} else {
this.chatScrollId = 'chat-msg' + (this.chatMessages.length - 1);
}
}, 100);
},
// ===== 格式化 AI 回复内容 =====
formatChatContent(text) {
if (!text) return '';
let html = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/^[\-\•]\s+(.+)$/gm, '<div style="padding-left:12px;">• $1</div>');
html = html.replace(/^(\d+)\.\s+(.+)$/gm, '<div style="padding-left:12px;">$1. $2</div>');
html = html.replace(/\n/g, '<br>');
return html;
},
openUrl(url) {
if (url) window.open(url, '_blank');
},
fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
},
formatReviewResult(text) {
if (!text) return '';
let html = text;
// 处理 markdown 标题 ## / ###
html = html.replace(/^###\s+(.+)$/gm, '<h4 style="margin:16px 0 8px;color:#333;">$1</h4>');
html = html.replace(/^##\s+(.+)$/gm, '<h3 style="margin:20px 0 10px;color:#222;">$1</h3>');
// 处理加粗
html = html.replace(/\*\*(.*?)\*\*/g, '<strong style="color:#a92924;">$1</strong>');
// 处理链接(PDF下载链接等)— 自动识别 http/https 链接
html = html.replace(/(https?:\/\/[^\s\)\]\"\<]+)/g, '<a href="$1" target="_blank" style="color:#a92924;text-decoration:underline;word-break:break-all;">$1</a>');
// 处理列表
html = html.replace(/^[\-\•]\s+(.+)$/gm, '<div style="padding-left:16px;margin:4px 0;">• $1</div>');
html = html.replace(/^(\d+)\.\s+(.+)$/gm, '<div style="padding-left:16px;margin:4px 0;">$1. $2</div>');
// 处理表格行(简单的 | 分隔表格)
html = html.replace(/\|(.+)\|/g, function(match) {
const cells = match.split('|').filter(c => c.trim());
if (cells.length > 1) {
return '<div style="display:flex;gap:8px;padding:6px 0;border-bottom:1px solid #eee;">' +
cells.map(c => '<span style="flex:1;font-size:13px;">' + c.trim() + '</span>').join('') + '</div>';
}
return match;
});
// 换行
html = html.replace(/\n/g, '<br>');
return html;
},
// ===== 积分相关 =====
// 积分不足时跳转到「积分管理」并刷新余额与明细
goRecharge() {
this.menuIndex = '6';
this.isSign = false;
this.isSignIntro = false;
this.isSignLog = false;
this.isSignFlow = false;
this.isSignTips = false;
this.getPointsBalance();
this.getPointsLog();
},
async getPointsBalance() {
try {
const res = await vk.callFunction({
url: 'client/points/kh/getBalance'
});
if (res.code === 0) {
this.pointsBalance = res.data.balance || 0;
this.pointsTotalEarned = res.data.total_earned || 0;
}
} catch (e) {
/* 静默 */ }
},
async getPointsLog() {
try {
const res = await vk.callFunction({
url: 'client/points/kh/getLog',
data: {
pageIndex: 1,
pageSize: 50
}
});
if (res.code === 0) {
this.pointsLogList = (res.data.rows || []).map(r => ({
...r,
_add_time_str: dayjs(r._add_time && r._add_time['$date'] ? r._add_time['$date'] : r._add_time).format('YYYY-MM-DD HH:mm:ss')
}));
}
} catch (e) {
/* 静默 */ }
},
async getReportList() {
try {
const reports = JSON.parse(localStorage.getItem('jjx_review_reports') || '[]');
this.reportList = reports.map(r => ({
fileName: r.fileName,
fileSize: r.fileSize,
time: r.time,
amount: r.points,
result: r.result,
status: 'completed'
}));
} catch (e) { /* 静默 */ }
},
async queryReports() {
try {
const res = await vk.callFunction({
url: 'client/ai/pub/getMyReports',
data: { pageIndex: 1, pageSize: 50 }
});
if (res.code === 0 && res.data && res.data.rows) {
this.reportList = res.data.rows.map(r => ({
fileName: r.file_name,
fileSize: r.file_size,
fileUrl: r.file_url || '',
htmlUrl: r.html_url || '',
time: dayjs(r._add_time && r._add_time['$date'] ? r._add_time['$date'] : r._add_time).format('YYYY-MM-DD HH:mm:ss'),
amount: r.points_used || 0,
result: r.result,
status: r.status,
recordId: r._id
}));
this.$message.success('加载了 ' + this.reportList.length + ' 条报告');
}
} catch (e) {
this.$message.error('查询失败');
}
},
viewReport(r) {
// 跳转到合同审查页并显示该报告的结果
this.menuIndex = '4';
this.reviewResult = r.result || '暂无审查结果';
this.reviewFile = { name: r.fileName, size: r.fileSize || 0 };
},
previewFile(r) {
if (!r.fileUrl) {
this.$message.warning('文件链接不存在');
return;
}
// 新窗口打开文件(PDF直接预览,Word下载)
window.open(r.fileUrl, '_blank');
},
formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
},
// ===== 合同审查相关 =====
selectReviewFile() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
if (file.size > 20 * 1024 * 1024) {
this.$message.error('文件大小超过 20MB');
return;
}
this.reviewFile = file;
this.reviewFileId = null;
this.reviewPoints = Math.max(200, Math.ceil(file.size / (100 * 1024)) * 200);
// 立即上传文件到扣子
this.reviewUploading = true;
try {
const fileBase64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
const res = await vk.callFunction({
url: 'client/ai/pub/review',
data: {
file_base64: fileBase64,
file_name: file.name,
file_size: file.size,
upload_only: true
}
});
if (res.code === 0 && res.data && res.data.file_id) {
this.reviewFileId = res.data.file_id;
this.reviewRecordId = res.data.record_id; // 保存待审查记录ID
this.$message.success('文件上传成功');
} else {
this.$message.error(res.msg || '文件上传失败');
this.reviewFile = null;
}
} catch (err) {
this.$message.error('文件上传异常');
this.reviewFile = null;
}
this.reviewUploading = false;
};
input.click();
},
// 前端 startReview() 完整改造
async startReview() {
if (!this.reviewFileId) {
this.$message.warning('请先选择合同文件并等待上传完成');
return;
}
// 1. 检查积分
const balanceRes = await vk.callFunction({
url: 'client/points/kh/getBalance'
});
if (balanceRes.code !== 0 || !balanceRes.data || balanceRes.data.balance < 200) {
this.$message.warning('积分不足,请先充值');
return;
}
this.reviewLoading = true;
// 2. 发起审查(更新已有记录状态为 reviewing,用 selectReviewFile 时保存的 file_id)
const reviewRes = await vk.callFunction({
url: 'client/ai/pub/review',
data: {
record_id: this.reviewRecordId,
file_id: this.reviewFileId,
file_name: this.reviewFile.name,
file_size: this.reviewFile.size,
uid: this.userInfo.uid
}
});
this.reviewLoading = false;
if (reviewRes.code !== 0) {
this.$message.error(reviewRes.msg || '发起审查失败');
return;
}
// 3. 提示用户 + 跳转到我的报告
this.$alert(
'合同已提交审查,AI 正在分析中,预计 3-5 分钟完成。您可以在「我的报告」中查看审查结果。',
'审查已提交',
{
confirmButtonText: '查看我的报告',
callback: () => {
this.menuSelect('5');
}
}
);
// 重置上传区
this.resetReview();
},
resetReview() {
this.reviewFile = null;
this.reviewFileId = null;
this.reviewRecordId = null;
this.reviewUploading = false;
this.reviewResult = null;
this.reviewProgress = 0;
this.reviewConversationId = null;
},
async doRecharge() {
const pkg = this.rechargePackages[this.selectedRecharge];
const priceMap = {
'20元': 2000, '100元': 10000, '200元': 20000,
'900元': 90000, '2000元': 200000, '4000元': 400000
}
const price = priceMap[pkg.price] || 2000;
const pointsMatch = pkg.points;
try {
// 创建充值订单
const res = await vk.callFunction({
url: 'client/points/kh/createOrder',
data: {
points: pointsMatch,
price: price
}
});
if (res.code === 0 && res.data) {
// 跳转到支付页面(复用现有银联支付)
vk.navigateTo({
url: '/pages/index/shop/pay?id=' + res.data
});
} else {
this.$message.error(res.msg || '创建订单失败');
}
} catch (e) {
this.$message.error('充值异常,请稍后重试');
}
},
changeAuthInfo() {
vk.navigateTo({url: '/pages/user/user?menu=fadada'})
},
async getSignFlow() {
const res = await vk.callFunction({
url: 'client/dict/pub/getDetail',
data: {
id: 'd77d384f686b9bd204bd4dc744cf75d0'
},
})
this.signFlow = res.content
this.signFlowTitle = res.name
},
async selectSignTips() {
this.isSign = false
this.isSignIntro = false
this.isSignLog = false
this.isSignTips = true
this.isSignFlow = false
this.menuIndex = ''
const res = await vk.callFunction({
url: 'client/dict/pub/getDetail',
data: {
id: '614937966875c18505570c357541c6e0'
},
})
this.signTips = res.content
},
async getSignLog(pageIndex) {
this.signLogType = 'log'
this.signLog = await vk.callFunction({
url: 'client/qyb/list/kh/payList',
data: {
pageIndex: pageIndex,
pageSize: 10
}
})
this.signLog.rows = this.signLog.rows.filter(item => !!item.contract)
this.$forceUpdate()
},
async getSignBuyLog(pageIndex) {
this.isSign = false
this.isSignIntro = false
this.isSignFlow = false
this.isSignLog = true
this.isSignTips = false
this.menuIndex = ''
this.signLogType = 'payLog'
this.signBuyLog = await vk.callFunction({
url: 'client/order/kh/getList',
data: {
isSeller: 0,
keyword: null,
orderStatus: 4,
pageIndex,
pageSize: 10,
signMethod: null,
type: [12, 13]
}
})
this.$forceUpdate()
},
async selectSignIntro(pageIndex) {
this.isSign = false
this.isSignIntro = true
this.isSignLog = false
this.isSignFlow = false
this.isSignTips = false
this.menuIndex = ''
const res = await vk.callFunction({
url: 'client/dict/pub/getDetail',
data: {
id: 'ef45322e671cece4110ec73b36ca2b7d'
},
})
this.signIntro = res.content
},
async query(pageIndex) {
this.tableData = await vk.callFunction({
url: 'client/contract/kh/getList',
title: '请求中...',
data: {
pageIndex,
pageSize: 10,
status: this.formData.status,
tradeMethod: this.formData.tradeMethod,
type: this.formData.type,
keyword: this.keyword,
signType: this.signTypeFilter === '1' ? '1' : '2',
},
});
// this.tableData.rows.forEach(item => {
// if (item.contract.type === 'sign' && !item.contract.payUser.user) console.log(item.contract.docTitle, item.contract._id)
// })
this.$forceUpdate()
},
/**
* 创建合同
* @param contract
*/
createContract(contract) {
this.form1.data.contractId = contract._id
// this.form1.props.action = 'client/contract/kh/launch'
this.form1.props.show = true
},
async changeContract(contract) {
this.form1.data.contractId = contract._id
const {data} = await vk.callFunction({
url: 'client/contract/kh/getDetail',
title: '请求中...',
data: {
contractId: contract._id
},
});
this.form1.data.signEndDate = dayjs(data.signEndDate).toDate()
this.form1.data.title = data.docTitle
// this.form1.data.contractFile = [data.contractBase]
if (Array.isArray(data.contractBase)) this.form1.data.contractFile = data.contractBase
else this.form1.data.contractFile = [data.contractBase]
console.log('this.form1.data.contractFile', this.form1.data.contractFile)
this.form1.data.buyerSignKeyword = data.buyerSignKeyword
this.form1.data.sellerSignKeyword = data.sellerSignKeyword
this.form1.data.otherFile = data.otherFile
this.form1.props.show = true
this.form1.data.thirdList = data.partyList.filter(item => item.role === 'third').map(item => {
return {
phone: item.user.mobile,
signKeyword: item.keyword
}
})
console.log(this.form1.data.thirdList)
if (!this.form1.data.thirdList || !this.form1.data.thirdList.length) {
this.form1.data.thirdList = [{
phone: '',
signKeyword: ''
}]
}
},
onFormSuccess() {
this.form1.props.show = false
// this.form1.data.contractId = null
// this.formData.status = 1
// this.menuIndex = '1'
// this.isSign = false
// this.$refs.form2.resetForm()
// this.$refs.form1.resetForm()
// this.query(1)
vk.redirectTo({url: '/pages/contract/index?tab=1'})
},
/**
* 签署合同
* @param sign
* @param contract
*/
async extSign(sign, contract) {
if (sign) return
try {
await this.$confirm(`1、您确认,若全体各签约方未完成全部签约流程,本签约文件不发生法律效力,系统无法对该签约文件予以保存及提供司法调证服务;<br>2、您确认,当前签约的实名主体名称、手机号码与注册认证阶段提交的信息未发生变更;若发生变更,必须及时进行重新认证后方可继续签约,避免产生严重法律风险与经济损失;<br>3、您确认,本次签约为本系统实名认证主体操作,如若不是实名认证主体操作,授权委托人需本系统进行授权认证后签约;<br>4、您确认,您与其他签约方均系在平等、自愿的基础上完成本次签约;<br>5、你确认,此次签约内容符合《中华人民共和国电子签名法》《民法典》生效保护范畴;<br>6、如因法律法规、监管部门政策更新等原因,系统提示您重新进行实名认证、证书续期等重要提示时,请您必须在签约前完成,否则造成任何损失,平台不承担任何责任;以上签约条件您均已经知悉并确认,否则您有权拒绝本次签约。`, '提示', {
confirmButtonText: '知悉同意',
showCancelButton: false,
type: 'warning',
dangerouslyUseHTMLString: true
})
} catch (e) {
return
}
vk.callFunction({
url: 'client/contract/kh/extSign',
title: '请求中...',
data: {
contractId: contract._id
},
success: (res) => {
window.open(res.data)
}
});
},
showStopSign(contract) {
vk.callFunction({
url: 'client/contract/kh/extSign',
title: '请求中...',
data: {
contractId: contract._id
},
success: (res) => {
window.open(res.data)
}
});
},
viewContract(contract) {
vk.callFunction({
url: 'client/contract/kh/viewContract',
title: '请求中...',
data: {
contractId: contract._id
},
success: (res) => {
const userAgent = window.navigator.userAgent.toLowerCase();
// if (userAgent.match(/micromessenger/i)) {
// window.open(`https://www.jinjiuxinzg.cn/pdfjs/web/viewer.html?file=${encodeURIComponent(res.data)}`)
// } else {
window.open(res.data)
// }
// window.open(res.data)
this.query(1)
}
});
},
navTo(url) {
vk.navigateTo({
url: url
})
},
showParty(userContract) {
// let content = ''
// userContract.contract.partyList.forEach(party => {
// // if (party.userId !== userContract.userId) partyUser = party.user
// const partyUser = party.user
// if (partyUser) {
// const name = partyUser.nickname
// let role = party.role === 'buyer' ? '购买方' : '销售方'
// if (party.role === 'third') role = '第三方'
// content += `<view class="flex flex-col justify-start items-start py-10 border-bottom">
// <view>身份:${role}</view>
// <view>联系人:${name}</view>
// <view>联系方式:${partyUser.mobile}</view></view>`
// }
// })
let content = ''
const buyer = userContract.contract.partyList.find(item => item.role !== 'third' && userContract.contract.userId === item.userId)
const seller = userContract.contract.partyList.find(item => item.role !== 'third' && userContract.contract.userId !== item.userId)
const thirdList = userContract.contract.partyList.filter(item => item.role === 'third')
content += `<view class="flex flex-col justify-start items-start py-10 border-bottom">
<view>身份:发起方</view>
<view>联系人:${(buyer && buyer.user) ? buyer.user.nickname : ''}</view>
<view>联系方式:${(buyer && buyer.user) ? buyer.user.mobile : ''}</view></view>`
content += `<view class="flex flex-col justify-start items-start py-10 border-bottom">
<view>身份:接收方</view>
<view>联系人:${(seller && seller.user) ? seller.user.nickname : ''}</view>
<view>联系方式:${(seller && seller.user) ? seller.user.mobile : ''}</view></view>`
thirdList.forEach((item, index) => {
content += `<view class="flex flex-col justify-start items-start py-10 border-bottom">
<view>身份:第${this.numToChForThird[index]}方</view>
<view>联系人:${item.user ? item.user.nickname : ''}</view>
<view>联系方式:${item.user ? item.user.mobile : ''}</view></view>`
})
this.$confirm(content, '', {
cancelButtonText: '关闭',
showConfirmButton: false,
dangerouslyUseHTMLString: true
})
},
changeTradeMethod(tradeMethod) {
this.formData.tradeMethod = tradeMethod
this.formData.type = null
this.query(1)
},
changeType(type) {
this.formData.tradeMethod = null
this.formData.type = type
// if (type === 'sign' && this.userInfo.qybNum === 0) {
// this.$confirm(`金久信签约宝次数已用完,请先购买`, '', {
// confirmButtonText: '去购买',
// })
// }
this.query(1)
},
openDownload(otherFile) {
const userAgent = window.navigator.userAgent.toLowerCase();
for (let file in otherFile) {
if (userAgent.match(/micromessenger/i)) {
window.open(`https://www.jinjiuxinzg.cn/pdfjs/web/viewer.html?file=${encodeURIComponent(file)}`)
} else {
window.open(file)
}
// window.open(otherFile)
}
},
async getCommonContractUrl() {
const res = await vk.callFunction({
url: 'client/dict/pub/getDetail',
data: {
id: '657d36befe975f63af1f24e7'
},
})
this.contractTemplate = res.content
},
openPreview(url) {
let openUrl = url
if (Array.isArray(url)) openUrl = url[url.length - 1]
if (!openUrl) return
window.open(`https://www.jinjiuxinzg.cn/pdfjs/web/viewer.html?file=${encodeURIComponent(openUrl)}`, '_blank')
},
downloadContractTemplate() {
window.open(this.contractTemplate)
},
async delContract(contract) {
this.$confirm('请您确认文件无用或已经完成履约后再删除文件,删除后平台将无法为您提供相关数据,您将无法查询已经删除文件。请您谨慎操作。', '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
await vk.callFunction({
url: 'client/contract/kh/del',
data: {
id: contract._id
},
success: (res) => {
this.query(1)
}
})
this.getUserInfo()
})
},
async payDownloadContract(contract) {
const amountRes = await vk.callFunction({
url: 'client/dict/pub/getDetail',
data: {
id: 'ded3e568662e5817000026633aad1e69'
},
})
if (!contract.canDownloadUidList || !contract.canDownloadUidList.includes(this.userInfo.uid)) {
console.log(contract.certificate)
if (contract.certificate && !contract.certificate.paidConfirm) {
if (contract.certificate.paymentMethod === 'online') {
return vk.navigateTo({
url: `/pages/index/shop/pay?id=${contract.certificate.orderId}`
})
} else return vk.toast('请先等待后台确认支付')
}
vk.navigateTo({
url: `/pages/contract/certificate?contractId=${contract._id}&transactionId=${contract.transactionId}&amount=${amountRes.content}`
})
} else {
this.$confirm(`您提交申请后,平台将在两个工作日内向您提供合法生效文件,客服电话:400-686-5559`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
// let loadingInstance1 = Loading.service({fullscreen: true, text: '加载中...'});
// const res = await vk.callFunction({
// url: 'client/contract/kh/report',
// data: {
// contractNum: contract.transactionId
// }
// });
// loadingInstance1.close()
// if (res) {
// window.open(res.data.downloadUrl)
// }
if (contract.certificate) {
if (contract.certificate.deliveryMethods.paper) {
vk.toast('您的司法调证文件已发货,请注意查收')
} else {
if (contract.certificate.downloadUrl) window.open(contract.certificate.downloadUrl)
else vk.toast('文件生成中,请耐心等待')
}
} else vk.toast('操作失败,请联系客服')
})
}
},
async downloadContract(contract_id) {
let loadingInstance1 = Loading.service({fullscreen: true, text: '加载中...'});
const res = await vk.callFunction({
url: 'client/contract/pub/getDownload',
data: {
contract_id
}
});
loadingInstance1.close()
if (res) {
window.open(`https://textapi.fadada.com/api2/downLoadContract.api?app_id=502704&timestamp=${res.data.timestamp}&v=2.0&msg_digest=${res.data.msg_digest}&contract_id=${contract_id}`)
}
},
getUserInfo() {
vk.userCenter.getCurrentUserInfo({
success: ({userInfo}) => {
this.userInfo = userInfo
this.invoiceForm.phone = this.userInfo.mobile
this.invoiceForm.email = this.userInfo.businessCard ? this.userInfo.businessCard.email : ''
if (this.userInfo.accountType === 2) {
if (this.userInfo.fadadaConfirm && this.userInfo.company) {
this.invoiceForm.name = this.userInfo.company.companyName
this.invoiceForm.code = this.userInfo.company.organization
this.invoiceForm.address = this.userInfo.company.addressDetail
this.invoiceForm.tel = this.userInfo.company.mobile
this.invoiceForm.bankName = this.userInfo.company.companyBankName
this.invoiceForm.bankAccount = this.userInfo.company.companyAccount
}
} else {
this.invoiceForm.name = this.userInfo.person.personName
this.invoiceForm.code = this.userInfo.person.idCard
this.invoiceForm.address = this.userInfo.person.address
this.invoiceForm.tel = this.userInfo.mobile
}
}
});
},
async getQybPackage() {
const {rows} = await vk.callFunction({
url: 'client/qyb/list/pub/getList',
data: {
pageIndex: 1,
pageSize: 500
}
});
this.qybPackageList = rows
},
async doBuyQyb() {
const {data} = await vk.callFunction({
url: 'client/qyb/list/kh/buy',
title: '请求中...',
data: {
qybId: this.selectedQyb,
}
})
this.dialogVisible = false
vk.navigateTo({
url: `/pages/index/shop/pay?id=${data}`
})
},
addThird() {
if (this.form2.data.thirdList.length < 8) this.form2.data.thirdList.push({
phone: '',
signKeyword: '',
})
},
removeThird(index) {
if (this.form2.data.thirdList.length === 1) return
this.form2.data.thirdList.splice(index, 1)
},
thirdMoveUp(index) {
let tmpList = JSON.parse(JSON.stringify(this.form2.data.thirdList));
[tmpList[index], tmpList[index - 1]] = [tmpList[index - 1], tmpList[index]];
this.form2.data.thirdList = tmpList
this.$forceUpdate()
},
thirdMoveDown(index) {
let tmpList = JSON.parse(JSON.stringify(this.form2.data.thirdList));
[tmpList[index], tmpList[index + 1]] = [tmpList[index + 1], tmpList[index]];
this.form2.data.thirdList = tmpList
this.$forceUpdate()
},
addThird1() {
if (this.form1.data.thirdList.length < 8) this.form1.data.thirdList.push({
phone: '',
signKeyword: '',
})
},
removeThird1(index) {
if (this.form1.data.thirdList.length === 1) return
this.form1.data.thirdList.splice(index, 1)
},
thirdMoveUp1(index) {
let tmpList = JSON.parse(JSON.stringify(this.form1.data.thirdList));
[tmpList[index], tmpList[index - 1]] = [tmpList[index - 1], tmpList[index]];
this.form1.data.thirdList = tmpList
this.$forceUpdate()
},
thirdMoveDown1(index) {
let tmpList = JSON.parse(JSON.stringify(this.form1.data.thirdList));
[tmpList[index], tmpList[index + 1]] = [tmpList[index + 1], tmpList[index]];
this.form1.data.thirdList = tmpList
this.$forceUpdate()
},
openInvoice(id) {
this.invoiceForm.id = id
this.showInvoice = true
},
async doInvoice() {
if (!this.invoiceForm.name) return this.$message.error('请输入发票抬头')
if (!this.invoiceForm.code) return this.$message.error('请输入纳税人识别号')
if (!this.invoiceForm.phone) return this.$message.error('请输入手机号码')
if (!this.invoiceForm.email) return this.$message.error('请输入邮箱')
if (!this.invoiceForm.address) return this.$message.error('请输入地址')
await vk.callFunction({
url: 'client/order/kh/changeInvoice',
data: {
...this.invoiceForm,
isInvoice: 1
}
})
this.showInvoice = false
this.$message.success('申请成功')
await this.getSignBuyLog(1)
},
copyInvoice(row) {
// const url = row.invoiceUrl[0]
// document.oncopy = function (e) {
// e.clipboardData.setData('text', url);
// e.preventDefault();
// document.oncopy = null;
// }
// document.execCommand('Copy');
// this.$message({
// type: 'success',
// message: '已复制'
// })
let openUrl = row.invoiceUrl
if (Array.isArray(openUrl)) openUrl = openUrl[openUrl.length - 1]
const userAgent = window.navigator.userAgent.toLowerCase();
// 检测 userAgent 中是否包含 'micromessenger' 字符串
if (userAgent.match(/micromessenger/i)) {
window.open(`https://www.jinjiuxinzg.cn/pdfjs/web/viewer.html?file=${encodeURIComponent(openUrl)}`)
} else {
window.open(openUrl)
}
},
openPayVoucher(row) {
const userAgent = window.navigator.userAgent.toLowerCase();
// 检测 userAgent 中是否包含 'micromessenger' 字符串
if (userAgent.match(/micromessenger/i)) {
this.payVoucher = row.payVoucher
this.showPayVoucher = true
} else {
window.open(row.payVoucher)
}
// window.open(row.payVoucher)
},
beforeUpload(file) {
if ((new RegExp(/\s+/g).test(file.name))) {
vk.toast('文件名不能有空格')
return false
}
},
uploadSuccess(response, file, fileList) {
console.log(response, file, fileList)
this.openPreview(response.url)
this.$refs.uploader.clearFiles();
},
uploadSuccess1(response, file, fileList) {
console.log(response, file, fileList)
this.openPreview(response.url)
this.$refs.uploader1.clearFiles();
},
async form1Action() {
const thirdKeywords = []
for (let i = 0; i < this.form1.data.thirdList.length; i++) {
const item = this.form1.data.thirdList[i]
if (item.phone && item.signKeyword) {
thirdKeywords.push(item.signKeyword)
}
}
uni.showLoading({title: '查验中'})
let file = this.form1.data.contractFile[this.form1.data.contractFile.length - 1]
if (Array.isArray(file)) file = file[file.length - 1]
vk.request({
method: "POST",
url: 'https://hcsc.gxwebsoft.com/api/read-pdf',
header: {
"content-type": "application/json; charset=utf-8",
},
data: {
file,
buyerSignKeyword: this.form1.data.buyerSignKeyword,
sellerSignKeyword: this.form1.data.sellerSignKeyword,
thirdKeywords
},
success: async (pdfRes) => {
},
fail: (pdfRes) => {
uni.hideLoading()
this.$message.error(pdfRes.content.msg)
},
complete: async (pdfRes) => {
if (pdfRes.code !== 200) this.$message.error(pdfRes.content.msg)
else {
uni.hideLoading()
this.form1.props.loading = true
this.form1.data.thirdList = this.form1.data.thirdList.filter(item => item.phone && item.signKeyword)
if (this.form1.data.thirdList.length > 0) {
for (let i = 0; i < this.form1.data.thirdList.length; i++) {
const item = this.form1.data.thirdList[i]
if (item.phone === '') {
vk.toast('请填写手机号')
this.form1.props.loading = false
return false
}
if (item.signKeyword === '') {
vk.toast('请填写签章关键字')
this.form1.props.loading = false
return false
}
if (item.phone === this.form1.data.phone) {
vk.toast('第三方手机号不能与接收方手机号相同')
this.form1.props.loading = false
return false
}
}
}
if (!this.form1.data.signEndDate) {
vk.toast('请选择签署截止时间')
this.form1.props.loading = false
return false
}
if (dayjs(this.form1.data.signEndDate).isBefore(dayjs())) {
vk.toast('签署截止日期不能早于当前时间')
this.form1.props.loading = false
return false
}
const res = await this.$confirm(`1、您确认,若全体各签约方未完成全部签约流程,本签约文件不发生法律效力,系统无法对该签约文件予以保存及提供司法调证服务;<br>2、您确认,当前签约的实名主体名称、手机号码与注册认证阶段提交的信息未发生变更;若发生变更,必须及时进行重新认证后方可继续签约,避免产生严重法律风险与经济损失;<br>3、您确认,本次签约为本系统实名认证主体操作,如若不是实名认证主体操作,授权委托人需本系统进行授权认证后签约;<br>4、您确认,您与其他签约方均系在平等、自愿的基础上完成本次签约;<br>5、你确认,此次签约内容符合《中华人民共和国电子签名法》《民法典》生效保护范畴;<br>6、如因法律法规、监管部门政策更新等原因,系统提示您重新进行实名认证、证书续期等重要提示时,请您必须在签约前完成,否则造成任何损失,平台不承担任何责任;以上签约条件您均已经知悉并确认,否则您有权拒绝本次签约。`, '提示', {
confirmButtonText: '知悉同意',
showCancelButton: false,
type: 'warning',
dangerouslyUseHTMLString: true
})
if (res === 'confirm') {
await vk.callFunction({
url: 'client/contract/kh/launch',
data: this.form1.data
})
vk.redirectTo({url: '/pages/contract/index?tab=1'})
}
}
uni.hideLoading()
},
})
}
}
}
</script>
<style lang="scss">
.el-picker-panel.currentDatePickerClass {
.el-button--text.el-picker-panel__link-btn {
display: none;
}
}
.currentDatePickerClass > > > .el-picker-panel__link-btn {
display: none
}
</style>
<style scoped lang="scss">
.w-1200 {
width: 1200px;
margin: 20px auto;
}
.contract-list {
background-color: #FFFFFF;
padding: 10px 20px 20px;
}
.paginatio {
margin: 20px auto;
display: flex;
justify-content: center;
}
.contract-box {
display: flex;
justify-content: space-between;
border-bottom: solid 1px #e8e8e8;
padding: 10rpx 0;
}
.contract-right {
text-align: right;
font-size: 13px;
}
.trade-method {
display: flex;
margin-bottom: 20px;
border-bottom: solid 2px #f4f4f4;
}
.trade-method-item {
cursor: pointer;
font-size: 18px;
margin-right: 15px;
height: 40px;
line-height: 40px;
}
.trade-method-item.active {
color: #A92924;
border-bottom: solid 2px #A92924;
}
.contract-left-top {
display: flex;
align-items: center;
}
.contract-product {
margin: 0 10px;
}
.contract-left-center {
//height: 40px;
display: flex;
justify-content: flex-start;
align-items: flex-start;
color: #999999;
font-size: 12px;
flex-direction: column;
margin-top: 5px;
}
.contract-left-bottom {
display: flex;
}
.contract-status {
display: flex;
}
.contract-status-tag {
margin-left: 2px;
}
.sign {
padding: 20px 20px 20px 25px;
font-size: 14px;
.icon {
line-height: 18px;
}
}
.qyb-buy-title {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.qyb-package-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
margin-top: 18px;
}
.qyb-package-card {
min-height: 110px;
padding: 22px 12px;
border: 1px solid #ebeef5;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s;
&:hover {
border-color: #a92924;
box-shadow: 0 6px 18px rgba(169, 41, 36, 0.16);
}
p {
margin: 0;
}
}
.qyb-package-amount {
color: #333;
font-size: 20px;
font-weight: 600;
line-height: 28px;
white-space: nowrap;
}
.qyb-package-times {
margin-top: 10px !important;
color: #666;
font-size: 16px;
line-height: 24px;
white-space: nowrap;
}
.qyb-remain-num {
margin-top: 22px;
font-size: 16px;
}
.qyb-dialog-footer {
margin-top: 24px;
}
.buy-quote-mode-active {
border: 1px solid #a92924;
background-color: #fff3f3;
box-shadow: 0 6px 18px rgba(169, 41, 36, 0.2);
color: #333;
}
@media screen and (max-width: 768px) {
.qyb-package-grid {
gap: 10px;
}
.qyb-package-card {
min-height: 92px;
padding: 16px 6px;
}
.qyb-package-amount {
font-size: 16px;
line-height: 22px;
}
.qyb-package-times {
font-size: 13px;
line-height: 20px;
}
}
/* ===== 新增AI功能菜单组 ===== */
.menu-group-title {
padding: 8px 20px 4px;
font-size: 11px;
color: #999;
letter-spacing: 2px;
text-transform: uppercase;
}
/* AI 客服嵌入样式 */
.ai-chat-embed {
background: #fff;
border-radius: 8px;
display: flex;
flex-direction: column;
height: calc(100vh - 260px);
overflow: hidden;
}
.chat-header-bar {
padding: 14px 20px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-title {
font-size: 15px;
font-weight: 600;
}
.chat-subtitle {
font-size: 12px;
color: #999;
}
.chat-msg-list {
flex: 1;
padding: 16px 20px;
background: #f7f8fa;
}
.chat-row {
display: flex;
gap: 10px;
margin-bottom: 12px;
}
.chat-row.right {
flex-direction: row-reverse;
}
.chat-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #a92924, #d4574e);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 14px;
flex-shrink: 0;
}
.chat-bubble {
max-width: 65%;
padding: 10px 14px;
border-radius: 12px;
font-size: 13px;
line-height: 1.6;
word-break: break-word;
}
.chat-row:not(.right) .chat-bubble {
background: #fff;
border: 1px solid #eee;
border-top-left-radius: 4px;
}
.chat-row.right .chat-bubble {
background: #a92924;
color: #fff;
border-top-right-radius: 4px;
}
.chat-bubble.typing {
color: #999;
}
.quick-btns {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.quick-btn {
padding: 5px 12px;
border-radius: 14px;
font-size: 12px;
cursor: pointer;
border: 1px solid #a92924;
color: #a92924;
background: #fff;
transition: all 0.2s;
}
.quick-btn:hover {
background: #a92924;
color: #fff;
}
.chat-input-bar {
padding: 12px 20px;
border-top: 1px solid #eee;
display: flex;
gap: 10px;
background: #fff;
}
.chat-input-bar input {
flex: 1;
height: 38px;
border: 1px solid #ddd;
border-radius: 8px;
padding: 0 12px;
font-size: 13px;
outline: none;
}
.chat-input-bar input:focus {
border-color: #a92924;
}
.chat-send-btn {
padding: 0 20px;
height: 38px;
line-height: 38px;
background: #a92924;
color: #fff;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
}
.chat-send-btn:hover {
background: #982520;
}
/* 合同审查样式 */
.review-container {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.review-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
.disclaimer-notice {
display: flex; align-items: flex-start; gap: 8px;
background: #fff8e1; border: 1px solid #ffe082; border-radius: 8px;
padding: 12px 16px; margin-bottom: 20px; font-size: 12px; color: #e6a23c;
}
.dn-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
.dn-text { line-height: 1.6; }
.points-notice {
display: flex;
align-items: center;
gap: 12px;
background: #fff8e1;
border: 1px solid #ffe082;
border-radius: 8px;
padding: 14px 20px;
margin-bottom: 20px;
}
.pn-icon {
font-size: 24px;
}
.pn-text {
flex: 1;
}
.pn-text .pn-title {
font-size: 13px;
font-weight: 600;
color: #333;
}
.pn-text .pn-desc {
font-size: 12px;
color: #666;
margin-top: 2px;
}
.pn-balance {
font-size: 18px;
font-weight: 700;
color: #a92924;
}
.upload-area {
border: 2px dashed #d9d9d9;
border-radius: 12px;
padding: 48px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
border-color: #a92924;
background: #fafafa;
}
.upload-area .upload-icon {
font-size: 48px;
margin-bottom: 16px;
}
.upload-area .upload-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
}
.upload-area .upload-desc {
font-size: 13px;
color: #999;
margin-bottom: 16px;
}
.upload-area .upload-btn {
display: inline-block;
padding: 10px 32px;
background: #a92924;
color: #fff;
border-radius: 8px;
font-size: 14px;
}
.file-info {
display: flex;
align-items: center;
gap: 12px;
background: #f5f5f5;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 16px;
}
.file-info .fi-icon {
font-size: 24px;
}
.file-info .fi-name {
font-size: 13px;
font-weight: 600;
}
.file-info .fi-size {
font-size: 11px;
color: #999;
}
.file-info .fi-remove {
margin-left: auto;
color: #f56c6c;
cursor: pointer;
font-size: 18px;
}
.review-opts {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.review-opt {
border: 1px solid #eee;
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.review-opt:hover {
border-color: #a92924;
}
.review-opt.selected {
border-color: #a92924;
background: #fef8f7;
}
.review-opt .opt-icon {
font-size: 28px;
margin-bottom: 8px;
}
.review-opt .opt-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
}
.review-opt .opt-desc {
font-size: 11px;
color: #999;
}
.review-opt .opt-points {
font-size: 12px;
color: #a92924;
font-weight: 600;
margin-top: 8px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.result-header h3 {
font-size: 14px;
}
.result-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
background: #e8f5e9;
color: #67c23a;
}
.result-body {
background: #f9f9f9;
border-radius: 8px;
padding: 20px;
font-size: 13px;
line-height: 1.8;
margin-bottom: 16px;
}
.result-footer {
text-align: center;
}
/* 积分管理样式 */
.points-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
.ps-card {
background: linear-gradient(135deg, #a92924, #d4574e);
border-radius: 12px;
padding: 24px;
color: #fff;
text-align: center;
}
.ps-card.secondary {
background: linear-gradient(135deg, #333, #555);
}
.ps-card .ps-label {
font-size: 12px;
opacity: 0.8;
margin-bottom: 8px;
}
.ps-card .ps-value {
font-size: 32px;
font-weight: 700;
}
/* 充值卡片样式 */
.recharge-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.recharge-card {
border: 2px solid #eee;
border-radius: 8px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.recharge-card:hover {
border-color: #a92924;
}
.recharge-card.selected {
border-color: #a92924;
background: #fef8f7;
}
.recharge-card .rc-points {
font-size: 24px;
font-weight: 700;
color: #a92924;
}
.recharge-card .rc-price {
font-size: 13px;
color: #666;
margin-top: 4px;
}
.recharge-card .rc-bonus {
font-size: 11px;
color: #fff;
background: #e6a23c;
padding: 2px 8px;
border-radius: 10px;
margin-top: 8px;
display: inline-block;
}
/* 我的报告样式 */
.report-item {
display: flex;
align-items: center;
padding: 14px 0;
border-bottom: 1px solid #f5f5f5;
}
.report-item .report-name {
font-size: 13px;
font-weight: 600;
}
.report-item .report-time {
font-size: 11px;
color: #999;
margin-top: 2px;
}
.report-item .report-info {
flex: 1;
}
</style>