zhangwencui
6 天以前 a8f88ef9b25238de27de6802e9fc2f79be809b48
feat(app): 对齐北方科技 CRM 规则
已修改5个文件
31 ■■■■■ 文件已修改
src/api/crm.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/crm/customer/form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/crm/customer/index.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/crm/invoice/detail.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/crm/invoice/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm.ts
@@ -7,7 +7,7 @@
export interface StorageFile { id: number; name?: string; url?: string; previewURL?: string; downloadURL?: string; }
export interface Clue { id?: number; name: string; source?: number; mobile?: string; telephone?: string; email?: string; wechat?: string; qq?: string; industryId?: number; level?: number; areaId?: number; areaName?: string; detailAddress?: string; contactNextTime?: string; remark?: string; ownerUserId?: number; ownerUserName?: string; ownerUserDeptName?: string; transformStatus?: boolean; followUpStatus?: boolean; contactLastTime?: string; contactLastContent?: string; creatorName?: string; createTime?: string; updateTime?: string; }
export interface CrmCustomerSimple { id: number; name: string; }
export interface CrmCustomer { id?: number; name: string; mobile?: string; telephone?: string; qq?: string; wechat?: string; email?: string; areaId?: number; areaName?: string; detailAddress?: string; industryId?: number; level?: number; source?: number; remark?: string; ownerUserId?: number; ownerUserName?: string; ownerUserDeptName?: string; lockStatus?: boolean; dealStatus?: boolean; followUpStatus?: boolean; contactStatus?: number; contactLastTime?: string; contactLastContent?: string; contactNextTime?: string; poolDay?: number; creatorName?: string; createTime?: string; updateTime?: string; }
export interface CrmCustomer { id?: number; name: string; mobile?: string; telephone?: string; qq?: string; wechat?: string; email?: string; areaId?: number; areaName?: string; detailAddress?: string; industryId?: number; level?: number; source?: number; remark?: string; ownerUserId?: number; ownerUserName?: string; ownerUserDeptName?: string; lockStatus?: boolean; dealStatus?: boolean; followUpStatus?: boolean; contactStatus?: number; contactLastTime?: string; contactLastContent?: string; contactNextTime?: string; /** 是否自动流入客户公海,默认开启 */ autoPoolEnabled?: boolean; poolDay?: number; creatorName?: string; createTime?: string; updateTime?: string; }
export interface CrmContact { id?: number; name: string; customerId?: number; customerName?: string; businessId?: number; ownerUserId?: number; mobile?: string; telephone?: string; email?: string; wechat?: string; qq?: string | number; post?: string; master?: boolean; contactLastTime?: string; contactLastContent?: string; contactNextTime?: string; ownerUserName?: string; ownerUserDeptName?: string; areaId?: number; areaName?: string; detailAddress?: string; sex?: number; parentId?: number; parentName?: string; remark?: string; creatorName?: string; createTime?: string; updateTime?: string; }
export interface ContractProduct { id?: number; itemId?: number; itemName?: string; itemCode?: string; itemSpecification?: string; itemUnitId?: number; itemUnitName?: string; itemPrice?: number; contractPrice?: number; count?: number; totalPrice?: number; }
export interface CrmContract { id?: number; name: string; no?: string; customerId?: number; customerName?: string; businessId?: number; businessName?: string; contactId?: number; contactName?: string; contactLastTime?: string; contactNextTime?: string; ownerUserId?: number; ownerUserName?: string; ownerUserDeptName?: string; processInstanceId?: number; auditStatus?: number; orderDate?: string; startTime?: string; endTime?: string; totalProductPrice?: number; discountPercent?: number; totalPrice?: number; depositPrice?: number; totalReceivablePrice?: number; totalInvoicePrice?: number; unInvoicePrice?: number; signContactId?: number; signContactName?: string; signUserId?: number; signUserName?: string; remark?: string; creatorName?: string; createTime?: string; updateTime?: string; products?: ContractProduct[]; }
src/pages/crm/customer/form.vue
@@ -5,7 +5,7 @@
import type { UserSimple } from '../../../api/purchase';
import { useAuth } from '../../../stores/auth';
const { state } = useAuth(); const id = ref<number>(); const saving = ref(false); const dicts = ref<DictData[]>([]); const users = ref<UserSimple[]>([]); const areas = ref<Area[]>([]);
const form = reactive<CrmCustomer>({ name: '', source: undefined, ownerUserId: undefined, mobile: '', telephone: '', email: '', wechat: '', qq: '', industryId: undefined, level: undefined, areaId: undefined, detailAddress: '', contactNextTime: '', remark: '' });
const form = reactive<CrmCustomer>({ name: '', source: undefined, ownerUserId: undefined, mobile: '', telephone: '', email: '', wechat: '', qq: '', industryId: undefined, level: undefined, areaId: undefined, detailAddress: '', contactNextTime: '', autoPoolEnabled: true, remark: '' });
const option = (type: string) => computed(() => dicts.value.filter(item => item.dictType === type && item.status !== 1).sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)));
const sources = option('crm_customer_source'); const industries = option('crm_customer_industry'); const levels = option('crm_customer_level');
function flatten(list: Area[], prefix = ''): Area[] { return list.flatMap(item => [{ ...item, name: `${prefix}${item.name}` }, ...flatten(item.children || [], `${prefix} `)]); }
@@ -15,5 +15,5 @@
async function save() { if (!form.name.trim() || form.source === undefined || !form.ownerUserId) return uni.showToast({ title: '请填写客户名称、客户来源和负责人', icon: 'none' }); if (!state.accessToken || saving.value) return; saving.value = true; try { const data = { ...form, name: form.name.trim(), contactNextTime: form.contactNextTime ? `${form.contactNextTime} 00:00:00` : undefined }; if (id.value) await updateCustomer(state.accessToken, { ...data, id: id.value }); else await createCustomer(state.accessToken, data); uni.showToast({ title: '保存成功', icon: 'success' }); setTimeout(() => uni.navigateBack(), 500); } catch (error) { uni.showToast({ title: error instanceof Error ? error.message : '保存失败', icon: 'none' }); } finally { saving.value = false; } }
onLoad(async query => { id.value = query.id ? Number(query.id) : undefined; if (!state.accessToken) return; try { const [dictionaryData, userData, areaData] = await Promise.all([getSimpleDictDataList(state.accessToken), getCrmUserList(state.accessToken), getAreaTree(state.accessToken)]); dicts.value = dictionaryData; users.value = userData; areas.value = areaData; if (id.value) { const data = await getCustomer(state.accessToken, id.value); Object.assign(form, data, { contactNextTime: data.contactNextTime?.slice(0, 10) || '' }); } else if (state.user) { form.ownerUserId = state.user.id; form.ownerUserName = state.user.nickname; } } catch (error) { uni.showToast({ title: error instanceof Error ? error.message : '数据加载失败', icon: 'none' }); } });
</script>
<template><view class="page"><view class="form-card"><view class="section">基本信息</view><view class="field"><text>客户名称 <text class="required">*</text></text><input v-model="form.name" placeholder="请输入客户名称" /></view><view class="field"><text>客户来源 <text class="required">*</text></text><picker :range="sources" range-key="label" @change="form.source=Number(sources[$event.detail.value].value)"><view class="picker">{{dictLabel(sources,form.source)}}<text>›</text></view></picker></view><view class="field"><text>负责人 <text class="required">*</text></text><picker :disabled="Boolean(id)" :range="users" range-key="nickname" @change="form.ownerUserId=users[$event.detail.value].id;form.ownerUserName=userLabel(users[$event.detail.value])"><view class="picker">{{form.ownerUserName||userLabel(users.find(item=>item.id===form.ownerUserId))}}<text>›</text></view></picker></view><view class="section">联系方式</view><view v-for="field in [['手机','mobile','请输入手机号'],['电话','telephone','请输入电话'],['邮箱','email','请输入邮箱'],['微信','wechat','请输入微信'],['QQ','qq','请输入 QQ']]" :key="field[1]" class="field"><text>{{field[0]}}</text><input v-model="form[field[1] as keyof CrmCustomer]" :placeholder="field[2]" /></view><view class="section">客户信息</view><view class="field"><text>客户行业</text><picker :range="industries" range-key="label" @change="form.industryId=Number(industries[$event.detail.value].value)"><view class="picker">{{dictLabel(industries,form.industryId)}}<text>›</text></view></picker></view><view class="field"><text>客户级别</text><picker :range="levels" range-key="label" @change="form.level=Number(levels[$event.detail.value].value)"><view class="picker">{{dictLabel(levels,form.level)}}<text>›</text></view></picker></view><view class="field"><text>所在地区</text><picker :range="areaOptions" range-key="name" @change="form.areaId=areaOptions[$event.detail.value].id;form.areaName=areaOptions[$event.detail.value].name.replace(/ /g,'')"><view class="picker">{{form.areaName||'请选择地区'}}<text>›</text></view></picker></view><view class="field"><text>详细地址</text><input v-model="form.detailAddress" placeholder="请输入详细地址" /></view><view class="field"><text>下次联系</text><picker mode="date" :value="form.contactNextTime" @change="form.contactNextTime=$event.detail.value"><view class="picker">{{form.contactNextTime||'请选择日期'}}<text>›</text></view></picker></view><view class="field column"><text>备注</text><textarea v-model="form.remark" placeholder="请输入备注" ></textarea></view></view><button class="save" :loading="saving" :disabled="saving" @click="save">保存</button></view></template>
<style lang="scss" scoped>.page{min-height:100vh;padding:24rpx 30rpx 70rpx;background:#f6f7f9}.form-card{overflow:hidden;background:#fff;border-radius:16rpx}.section{padding:22rpx 24rpx 12rpx;color:var(--color-primary, #1769d8);font-size:25rpx;font-weight:600;background:#f8fbff}.field{display:flex;align-items:center;min-height:96rpx;padding:0 24rpx;border-bottom:1rpx solid #eef1f5;gap:20rpx}.field>text{width:180rpx;color:#33455d;font-size:27rpx}.required{color:#e34b4b}.field input,.field picker,.picker{flex:1;color:#1e3049;font-size:27rpx}.picker{display:flex;justify-content:space-between;color:#65758a}.picker text{margin-left:16rpx;color:#8092a7;font-size:36rpx}.column{align-items:flex-start;flex-direction:column;padding:20rpx 24rpx}.column>text{width:auto}.column textarea{width:100%;height:130rpx;font-size:27rpx}.save{margin-top:32rpx;color:#fff;background:var(--color-primary, #1769d8);border-radius:12rpx;font-size:30rpx}.save::after{border:0}</style>
<template><view class="page"><view class="form-card"><view class="section">基本信息</view><view class="field"><text>客户名称 <text class="required">*</text></text><input v-model="form.name" placeholder="请输入客户名称" /></view><view class="field"><text>客户来源 <text class="required">*</text></text><picker :range="sources" range-key="label" @change="form.source=Number(sources[$event.detail.value].value)"><view class="picker">{{dictLabel(sources,form.source)}}<text>›</text></view></picker></view><view class="field"><text>负责人 <text class="required">*</text></text><picker :disabled="Boolean(id)" :range="users" range-key="nickname" @change="form.ownerUserId=users[$event.detail.value].id;form.ownerUserName=userLabel(users[$event.detail.value])"><view class="picker">{{form.ownerUserName||userLabel(users.find(item=>item.id===form.ownerUserId))}}<text>›</text></view></picker></view><view class="section">联系方式</view><view v-for="field in [['手机','mobile','请输入手机号'],['电话','telephone','请输入电话'],['邮箱','email','请输入邮箱'],['微信','wechat','请输入微信'],['QQ','qq','请输入 QQ']]" :key="field[1]" class="field"><text>{{field[0]}}</text><input v-model="form[field[1] as keyof CrmCustomer]" :placeholder="field[2]" /></view><view class="section">客户信息</view><view class="field"><text>客户行业</text><picker :range="industries" range-key="label" @change="form.industryId=Number(industries[$event.detail.value].value)"><view class="picker">{{dictLabel(industries,form.industryId)}}<text>›</text></view></picker></view><view class="field"><text>客户级别</text><picker :range="levels" range-key="label" @change="form.level=Number(levels[$event.detail.value].value)"><view class="picker">{{dictLabel(levels,form.level)}}<text>›</text></view></picker></view><view class="field"><text>是否流入公海</text><switch :checked="form.autoPoolEnabled !== false" @change="form.autoPoolEnabled=$event.detail.value" /></view><view class="tip">关闭后,客户不会自动流入公海。</view><view class="field"><text>所在地区</text><picker :range="areaOptions" range-key="name" @change="form.areaId=areaOptions[$event.detail.value].id;form.areaName=areaOptions[$event.detail.value].name.replace(/ /g,'')"><view class="picker">{{form.areaName||'请选择地区'}}<text>›</text></view></picker></view><view class="field"><text>详细地址</text><input v-model="form.detailAddress" placeholder="请输入详细地址" /></view><view class="field"><text>下次联系</text><picker mode="date" :value="form.contactNextTime" @change="form.contactNextTime=$event.detail.value"><view class="picker">{{form.contactNextTime||'请选择日期'}}<text>›</text></view></picker></view><view class="field column"><text>备注</text><textarea v-model="form.remark" placeholder="请输入备注" ></textarea></view></view><button class="save" :loading="saving" :disabled="saving" @click="save">保存</button></view></template>
<style lang="scss" scoped>.page{min-height:100vh;padding:24rpx 30rpx 70rpx;background:#f6f7f9}.form-card{overflow:hidden;background:#fff;border-radius:16rpx}.section{padding:22rpx 24rpx 12rpx;color:var(--color-primary, #1769d8);font-size:25rpx;font-weight:600;background:#f8fbff}.field{display:flex;align-items:center;min-height:96rpx;padding:0 24rpx;border-bottom:1rpx solid #eef1f5;gap:20rpx}.field>text{width:180rpx;color:#33455d;font-size:27rpx}.required{color:#e34b4b}.field input,.field picker,.picker{flex:1;color:#1e3049;font-size:27rpx}.picker{display:flex;justify-content:space-between;color:#65758a}.picker text{margin-left:16rpx;color:#8092a7;font-size:36rpx}.tip{padding:12rpx 24rpx;color:#8b99aa;font-size:22rpx;background:#f8fbff}.column{align-items:flex-start;flex-direction:column;padding:20rpx 24rpx}.column>text{width:auto}.column textarea{width:100%;height:130rpx;font-size:27rpx}.save{margin-top:32rpx;color:#fff;background:var(--color-primary, #1769d8);border-radius:12rpx;font-size:30rpx}.save::after{border:0}</style>
src/pages/crm/customer/index.vue
@@ -162,6 +162,7 @@
            <view class="meta-grid">
              <text>来源:{{dictLabel('crm_customer_source',item.source)}}</text><text>行业:{{dictLabel('crm_customer_industry',item.industryId)}}</text>
              <text>级别:{{dictLabel('crm_customer_level',item.level)}}</text><text>负责人:{{item.ownerUserName||'未分配'}}</text>
              <text>流入公海:{{item.autoPoolEnabled === false ? '否' : '是'}}</text>
              <text class="full">联系方式:{{contactText(item)}}</text><text class="full">地址:{{addressText(item)}}</text>
              <text>所属部门:{{item.ownerUserDeptName||'—'}}</text><text>下次联系:{{dateText(item.contactNextTime)}}</text>
              <text>最后跟进:{{dateText(item.contactLastTime)}}</text><text>创建人:{{item.creatorName||'—'}}</text>
src/pages/crm/invoice/detail.vue
@@ -1,4 +1,4 @@
<script setup lang="ts">
import { computed, ref } from 'vue'; import { onLoad, onShow } from '@dcloudio/uni-app'; import { getInvoice, submitInvoice, type CrmInvoice } from '../../../api/crm'; import { useAuth } from '../../../stores/auth';
const {hasPermission,state}=useAuth();const id=ref(0),item=ref<CrmInvoice>(),loading=ref(false);const canUpdate=computed(()=>hasPermission('crm:invoice:update'));const money=(x?:number)=>x===undefined?'—':`¥${Number(x).toFixed(2)}`;const status=(x?:number)=>({0:'未提交',10:'审批中',20:'审批通过',30:'审批不通过',40:'已取消',50:'已作废'} as Record<number,string>)[x??0]||'未知状态';async function load(){if(!state.accessToken||!id.value)return;loading.value=true;try{item.value=await getInvoice(state.accessToken,id.value);}catch(error){uni.showToast({title:error instanceof Error?error.message:'详情加载失败',icon:'none'});}finally{loading.value=false;}}async function submit(){if(!state.accessToken)return;try{await submitInvoice(state.accessToken,id.value);uni.showToast({title:'已提交审核',icon:'success'});load();}catch(error){uni.showToast({title:error instanceof Error?error.message:'提交失败',icon:'none'});}}onLoad(q=>{id.value=Number(q.id);load();});onShow(load);
</script><template><view class="page"><view v-if="loading" class="state">加载中...</view><template v-else-if="item"><view class="summary"><view><text>{{item.no||'开票申请'}}</text><text>{{item.customerName||'—'}} · {{money(item.price)}}</text></view><text>{{status(item.auditStatus)}}</text></view><view class="toolbar"><button v-if="canUpdate" @click="uni.navigateTo({url:`/pages/crm/invoice/form?id=${id}`})">编辑</button><button v-if="canUpdate&&item.auditStatus===0" @click="submit">提交审核</button></view><view class="card"><view v-for="row in [['客户',item.customerName],['合同',item.contract?.no||item.contract?.name],['负责人',item.ownerUserName],['所属部门',item.ownerUserDeptName],['发票类型',item.invoiceType],['发票号码',item.invoiceNo],['发票抬头',item.invoiceTitle],['开票金额',money(item.price)],['开票日期',item.invoiceTime?.slice(0,10)],['剩余可回款金额',money(item.remainingInvoicePrice)],['创建人',item.creatorName],['创建时间',item.createTime?.replace('T',' ').slice(0,16)]]" :key="row[0]" class="row"><text>{{row[0]}}</text><text>{{row[1]??'—'}}</text></view><view class="remark"><text>备注</text><text>{{item.remark||'—'}}</text></view><view class="remark"><text>发票附件</text><text v-if="item.attachmentList?.length">{{item.attachmentList.map(x=>x.name||'附件').join('、')}}</text><text v-else>未上传</text></view></view></template><view v-else class="state">暂无开票申请数据</view></view></template><style lang="scss" scoped>.page{min-height:100vh;padding:24rpx 30rpx;background:#f6f7f9}.summary{display:flex;align-items:center;justify-content:space-between;padding:26rpx;color:#fff;background:var(--color-primary, #1769d8);border-radius:16rpx}.summary text{display:block}.summary view text:first-child{font-size:32rpx;font-weight:600}.summary view text:last-child{margin-top:8rpx;font-size:24rpx;color:#dbeaff}.summary>text{padding:5rpx 10rpx;font-size:21rpx;background:rgba(255,255,255,.18);border-radius:8rpx}.toolbar{display:flex;justify-content:flex-end;gap:16rpx;padding:18rpx 0}.toolbar button{margin:0;color:var(--color-primary, #1769d8);font-size:24rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee)}.toolbar button::after{border:0}.card{background:#fff;border-radius:16rpx}.row{display:flex;justify-content:space-between;gap:24rpx;min-height:88rpx;align-items:center;padding:0 24rpx;border-bottom:1rpx solid #eef1f5}.row text:first-child,.remark>text:first-child{color:#718198;font-size:26rpx}.row text:last-child{color:#263a54;font-size:26rpx;text-align:right}.remark{padding:20rpx 24rpx;border-bottom:1rpx solid #eef1f5}.remark text{display:block}.remark text:last-child{margin-top:12rpx;color:#263a54;font-size:26rpx;line-height:1.6}.state{padding:80rpx 20rpx;color:#8795a8;font-size:26rpx;text-align:center}</style>
import { computed, ref } from 'vue'; import { onLoad, onShow } from '@dcloudio/uni-app'; import { getInvoice, submitInvoice, type CrmInvoice } from '../../../api/crm'; import { getAutoApprovalPolicy } from '../../../api/hrm'; import { useAuth } from '../../../stores/auth';
const {hasPermission,state}=useAuth();const id=ref(0),item=ref<CrmInvoice>(),loading=ref(false),automaticApproval=ref(false);const canUpdate=computed(()=>hasPermission('crm:invoice:update'));const canEdit=computed(()=>canUpdate.value&&(automaticApproval.value||item.value?.auditStatus===0||item.value?.auditStatus===10));const money=(x?:number)=>x===undefined?'—':`¥${Number(x).toFixed(2)}`;const status=(x?:number)=>({0:'未提交',10:'审批中',20:'审批通过',30:'审批不通过',40:'已取消',50:'已作废'} as Record<number,string>)[x??0]||'未知状态';async function load(){if(!state.accessToken||!id.value)return;loading.value=true;try{item.value=await getInvoice(state.accessToken,id.value);}catch(error){uni.showToast({title:error instanceof Error?error.message:'详情加载失败',icon:'none'});}finally{loading.value=false;}}async function loadApprovalPolicy(){if(!state.accessToken)return;automaticApproval.value=await getAutoApprovalPolicy(state.accessToken).catch(()=>false);}async function submit(){if(!state.accessToken)return;try{await submitInvoice(state.accessToken,id.value);uni.showToast({title:'已提交审核',icon:'success'});load();}catch(error){uni.showToast({title:error instanceof Error?error.message:'提交失败',icon:'none'});}}onLoad(q=>{id.value=Number(q.id);load();loadApprovalPolicy();});onShow(load);
</script><template><view class="page"><view v-if="loading" class="state">加载中...</view><template v-else-if="item"><view class="summary"><view><text>{{item.no||'开票申请'}}</text><text>{{item.customerName||'—'}} · {{money(item.price)}}</text></view><text>{{status(item.auditStatus)}}</text></view><view class="toolbar"><button v-if="canEdit" @click="uni.navigateTo({url:`/pages/crm/invoice/form?id=${id}`})">编辑</button><button v-if="canUpdate&&item.auditStatus===0" @click="submit">提交审核</button></view><view class="card"><view v-for="row in [['客户',item.customerName],['合同',item.contract?.no||item.contract?.name],['负责人',item.ownerUserName],['所属部门',item.ownerUserDeptName],['发票类型',item.invoiceType],['发票号码',item.invoiceNo],['发票抬头',item.invoiceTitle],['开票金额',money(item.price)],['开票日期',item.invoiceTime?.slice(0,10)],['剩余可回款金额',money(item.remainingInvoicePrice)],['创建人',item.creatorName],['创建时间',item.createTime?.replace('T',' ').slice(0,16)]]" :key="row[0]" class="row"><text>{{row[0]}}</text><text>{{row[1]??'—'}}</text></view><view class="remark"><text>备注</text><text>{{item.remark||'—'}}</text></view><view class="remark"><text>发票附件</text><text v-if="item.attachmentList?.length">{{item.attachmentList.map(x=>x.name||'附件').join('、')}}</text><text v-else>未上传</text></view></view></template><view v-else class="state">暂无开票申请数据</view></view></template><style lang="scss" scoped>.page{min-height:100vh;padding:24rpx 30rpx;background:#f6f7f9}.summary{display:flex;align-items:center;justify-content:space-between;padding:26rpx;color:#fff;background:var(--color-primary, #1769d8);border-radius:16rpx}.summary text{display:block}.summary view text:first-child{font-size:32rpx;font-weight:600}.summary view text:last-child{margin-top:8rpx;font-size:24rpx;color:#dbeaff}.summary>text{padding:5rpx 10rpx;font-size:21rpx;background:rgba(255,255,255,.18);border-radius:8rpx}.toolbar{display:flex;justify-content:flex-end;gap:16rpx;padding:18rpx 0}.toolbar button{margin:0;color:var(--color-primary, #1769d8);font-size:24rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee)}.toolbar button::after{border:0}.card{background:#fff;border-radius:16rpx}.row{display:flex;justify-content:space-between;gap:24rpx;min-height:88rpx;align-items:center;padding:0 24rpx;border-bottom:1rpx solid #eef1f5}.row text:first-child,.remark>text:first-child{color:#718198;font-size:26rpx}.row text:last-child{color:#263a54;font-size:26rpx;text-align:right}.remark{padding:20rpx 24rpx;border-bottom:1rpx solid #eef1f5}.remark text{display:block}.remark text:last-child{margin-top:12rpx;color:#263a54;font-size:26rpx;line-height:1.6}.state{padding:80rpx 20rpx;color:#8795a8;font-size:26rpx;text-align:center}</style>
src/pages/crm/invoice/index.vue
@@ -7,6 +7,7 @@
    submitInvoice,
    type CrmInvoice,
  } from "../../../api/crm";
  import { getAutoApprovalPolicy } from "../../../api/hrm";
  import { useAuth } from "../../../stores/auth";
  import { statusToneClass } from "../../../utils/status";
  const { hasPermission, isLoggedIn, state } = useAuth();
@@ -16,11 +17,15 @@
    pageNo = ref(1),
    total = ref(0),
    loading = ref(false),
    automaticApproval = ref(false),
    loadError = ref("");
  const canCreate = computed(() => hasPermission("crm:invoice:create")),
    canUpdate = computed(() => hasPermission("crm:invoice:update")),
    canDelete = computed(() => hasPermission("crm:invoice:delete")),
    hasMore = computed(() => items.value.length < total.value);
  const canEdit = (item: CrmInvoice) =>
    canUpdate.value &&
    (automaticApproval.value || item.auditStatus === 0 || item.auditStatus === 10);
  const money = (value?: number) =>
    value === undefined || value === null ? "—" : `¥${Number(value).toFixed(2)}`;
  const status = (value?: number) =>
@@ -100,7 +105,14 @@
      });
    }
  }
  onShow(() => load(true));
  async function loadApprovalPolicy() {
    if (!state.accessToken) return;
    automaticApproval.value = await getAutoApprovalPolicy(state.accessToken).catch(() => false);
  }
  onShow(() => {
    loadApprovalPolicy();
    load(true);
  });
  onPullDownRefresh(async () => {
    await load(true);
    uni.stopPullDownRefresh();
@@ -131,7 +143,7 @@
                @click="goDetail(item.id)">
            <view class="head"><text class="name">{{item.no||'待生成开票编号'}}</text><text :class="['tag', statusClass(item.auditStatus)]">{{status(item.auditStatus)}}</text></view>
            <view class="grid"><text>客户:{{item.customerName||'—'}}</text><text>合同:{{item.contract?.no||item.contract?.name||'—'}}</text><text>发票类型:{{item.invoiceType===undefined?'—':item.invoiceType}}</text><text>发票号码:{{item.invoiceNo||'—'}}</text><text>发票抬头:{{item.invoiceTitle||'—'}}</text><text>开票金额:{{money(item.price)}}</text><text>开票日期:{{item.invoiceTime?.slice(0,10)||'—'}}</text><text>剩余可开票:{{money(item.remainingInvoicePrice)}}</text><text>负责人:{{item.ownerUserName||'—'}}</text><text>所属部门:{{item.ownerUserDeptName||'—'}}</text><text>创建人:{{item.creatorName||'—'}}</text><text>创建时间:{{item.createTime?.replace('T',' ').slice(0,16)||'—'}}</text><text>更新时间:{{item.updateTime?.replace('T',' ').slice(0,16)||'—'}}</text><text>附件:{{item.hasAttachment?'已上传':'未上传'}}</text><text class="full">备注:{{item.remark||'—'}}</text></view>
            <view class="actions"><button v-if="canUpdate"
            <view class="actions"><button v-if="canEdit(item)"
                      @click.stop="goForm(item.id)">编辑</button><button v-if="canUpdate&&item.auditStatus===0"
                      @click.stop="submit(item)">提交审核</button><button v-if="canDelete"
                      class="danger"