上线申请

pull/781/head
zy 2025-05-12 21:57:45 +08:00
parent 1dda82a081
commit a963bdef6f
14 changed files with 1087 additions and 288 deletions

View File

@ -44,6 +44,10 @@ export const QuotationApi = {
getSimpleQuotationList : async () => {
return await request.get({ url: `/crm/quotation/simple-all-list` })
},
// 获得 CRM 方案营收分析
getRevenueList : async () => {
return await request.get({ url: `/crm/quotation/get-revenue-analysis` })
},
// 获得 CRM 方案列表(精简)
getSelfSimpleQuotationList : async () => {

View File

@ -0,0 +1,43 @@
import request from '@/config/axios'
// 上线模块信息 VO
export interface OnlineModuleInfoVO {
id: number // 唯一主键
onlineApplicationId: number // 上线申请ID
involvingModule: string // 涉及模块
version: string // 版本
moduleDescription: string // 模块说明
}
// 上线模块信息 API
export const OnlineModuleInfoApi = {
// 查询上线模块信息分页
getOnlineModuleInfoPage: async (params: any) => {
return await request.get({ url: `/tts/online-module-info/page`, params })
},
// 查询上线模块信息详情
getOnlineModuleInfo: async (id: number) => {
return await request.get({ url: `/tts/online-module-info/get?id=` + id })
},
// 新增上线模块信息
createOnlineModuleInfo: async (data: OnlineModuleInfoVO) => {
return await request.post({ url: `/tts/online-module-info/create`, data })
},
// 修改上线模块信息
updateOnlineModuleInfo: async (data: OnlineModuleInfoVO) => {
return await request.put({ url: `/tts/online-module-info/update`, data })
},
// 删除上线模块信息
deleteOnlineModuleInfo: async (id: number) => {
return await request.delete({ url: `/tts/online-module-info/delete?id=` + id })
},
// 导出上线模块信息 Excel
exportOnlineModuleInfo: async (params) => {
return await request.download({ url: `/tts/online-module-info/export-excel`, params })
}
}

View File

@ -43,7 +43,6 @@ import Logger from '@/utils/Logger'
import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患
import VueSignaturePad from "vue-signature-pad";
// import 'vue-signature-pad/dist/vue-signature-pad.css';
// 创建实例
const setupAll = async () => {

View File

@ -836,7 +836,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true,
activeMenu: '/project/project'
},
component: () => import('@/views/task/components/TaskDetail.vue')
component: () => import('@/views/task/project/components/TaskDetail.vue')
},
{
path: 'components/ProjectTaskForm',
@ -847,19 +847,41 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true,
activeMenu: '/project/project'
},
component: () => import('@/views/task/components/ProjectTaskForm.vue')
component: () => import('@/views/task/project/components/ProjectTaskForm.vue')
},
{
path: 'online/OnlineApplicationForm',
name: 'OnlineAppAdd',
meta: {
title: '上线新增',
title: '上线申请新增',
noCache: true,
hidden: true,
activeMenu: '/project/online'
},
component: () => import('@/views/task/online/OnlineApplicationForm.vue')
},
{
path: 'online/OnlineApplicationEdit',
name: 'OnlineAppEdit',
meta: {
title: '上线申请编辑',
noCache: true,
hidden: true,
activeMenu: '/project/online'
},
component: () => import('@/views/task/online/OnlineApplicationForm.vue')
},
{
path: 'online/OnlineApplicationDetail',
name: 'OnlineAppDetail',
meta: {
title: '上线申请详情',
noCache: true,
hidden: true,
activeMenu: '/project/online'
},
component: () => import('@/views/task/online/OnlineApplicationDetail.vue')
},
]
},
{

View File

@ -96,9 +96,9 @@
<el-input v-model="formData.consultantManagement" placeholder="请输入顾问管理" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
<el-input type="textarea" :rows="5" v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>

View File

@ -378,10 +378,13 @@
</el-row>
</el-form>
<!-- 子表的表单 -->
<el-tabs v-model="subTabsName">
<el-tabs v-model="subTabsName" @tab-Click="handleClick">
<el-tab-pane label="报价产品关联" name="quotationProduct">
<QuotationProductForm ref="quotationProductFormRef" :handleType="handleType" :quotation-id="formData.id" :business-id="formData.businessId" :billTemplateId="formData.invoiceTemplateId" :products="formData.products" @success="setSuccess" />
</el-tab-pane>
<el-tab-pane label="营收分析" name="RevenueAnalysis">
<RevenueAnalysis ref="RevenueAnalysisFormRef" :quotation-id="formData.id" :products="formData.products" />
</el-tab-pane>
</el-tabs>
<div style="text-align: right">
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@ -395,6 +398,7 @@ import { QuotationApi, QuotationVO } from '@/api/crm/quotation'
import { defaultProps, handleTree } from '@/utils/tree'
import { BillTemplateApi, BillTemplateVO } from '@/api/crm/billtemplate'
import QuotationProductForm from './components/QuotationProductForm.vue'
import RevenueAnalysis from './components/RevenueAnalysis.vue'
import * as ContractApi from '@/api/crm/contract'
import * as CustomerApi from '@/api/crm/customer'
import * as UserApi from '@/api/system/user'
@ -531,6 +535,11 @@ const setSuccess = (val) => {
formData.value.products = val
}
//
const handleClick = (val) => {
console.log('%csrc/views/crm/quotation/QuotationForm.vue:540 val', 'color: #007acc;', subTabsName.value);
}
const onBusinessChange = async (businessId: string) => {
if (!businessId) return

View File

@ -0,0 +1,85 @@
<template>
<el-table :data="formData" class="-mt-10px">
<el-table-column label="产品类型" align="center" min-width="150">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.CRM_PRODUCT_CATEGORY" :value="row.category" />
</template>
</el-table-column>
<el-table-column label="产品明细类型" align="center" min-width="190">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.CRM_PRODUCT_DETAIL_TYPE" :value="row.detailType" />
</template>
</el-table-column>
<el-table-column label="销售额" align="center" min-width="150">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="row.productUnit" />
</template>
</el-table-column>
<el-table-column label="订单量" align="center" min-width="150">
<template #default="{ row }">
{{row.onlinePrice}}
</template>
</el-table-column>
<el-table-column label="营业收入" align="center" min-width="150">
<template #default="{ row }">
{{row.offlinePrice}}
</template>
</el-table-column>
<el-table-column label="收入利率" align="center" min-width="130">
<template #default="{ row }">
{{row.downlinePrice}}
</template>
</el-table-column>
<el-table-column label="应扣成本" align="center" min-width="150">
<template #default="{ row }">
{{row.totalPrice}}
</template>
</el-table-column>
<el-table-column label="利润" align="center" min-width="150">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.CRM_PRODUCT_INVOICE" :value="row.productInvoice" />
</template>
</el-table-column>
<el-table-column label="利润率" align="center" min-width="250">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.CRM_PRODUCT_INVOICE_ITEMS" :value="row.productInvoiceItem" />
</template>
</el-table-column>
<el-table-column label="您的分润比例" align="center" min-width="150">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.CRM_SERVICE_FEE_INVOICE" :value="row.serviceInvoice" />
</template>
</el-table-column>
<el-table-column label="您的分润收入" align="center" min-width="250">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.CRM_SERVICE_FEE_INVOICE_ITEMS" :value="row.serviceInvoiceItem" />
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import { QuotationApi, QuotationVO } from '@/api/crm/quotation'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as BusinessApi from '@/api/crm/business'
const props = defineProps<{
quotationId: undefined, //
products: undefined
}>()
const formLoading = ref(false) //
const formData = ref([])
/** 表单值 */
const getData = async (data) => {
formData.value = await QuotationApi.getQuotation(data)
}
const getName = (opt, val) => {
const arr = opt.filter(v => v.value == val)
return arr.length ? arr[0]['label'] : ''
}
defineExpose({ getData })
</script>

View File

@ -0,0 +1,74 @@
<template>
<div class="signature-container">
<h5>请在下方框内签名</h5>
<div class="signature-pad-container">
<VueSignaturePad
width="300px"
height="150px"
ref="signaturePad"
/>
</div>
<div class="signature-actions">
<el-button type="button" @click="close"></el-button>
<el-button type="button" @click="clear"></el-button>
<el-button type="button" @click="save"></el-button>
</div>
<!-- <div v-if="signatureUrl" class="signature-preview">
<el-image :src="signatureUrl" alt="签名预览" />
</div> -->
</div>
</template>
<script setup>
import { ref } from 'vue';
const signaturePad = ref(null);
const signatureUrl = ref('');
const emit = defineEmits(['success']) // success
const save = () => {
const { isEmpty, data } = signaturePad.value.saveSignature();
if (!isEmpty) {
console.log(data); // Base64
// signatureUrl.value = data;
emit('success', data)
} else {
alert('请先签名');
}
};
const close = () => {
emit('close')
}
const clear = () => {
signaturePad.value.clearSignature();
signatureUrl.value = '';
};
</script>
<style scoped>
.signature-container {
max-width: 800px;
margin: 0 auto;
}
.signature-pad-container {
border: 1px solid #dcdfe6;
border-radius: 4px;
margin-bottom: 20px;
}
.signature-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 20px;
}
.signature-preview {
margin-top: 20px;
text-align: center;
}
</style>

View File

@ -0,0 +1,277 @@
<template>
<ContentWrap>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="130px"
v-loading="formLoading"
>
<el-row>
<el-col :span="8">
<el-form-item label="产品名称" prop="projectName">
<el-input v-model="formData.projectName" disabled placeholder="请输入产品名称" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目类型" prop="projectType">
<el-select v-model="formData.projectType" disabled placeholder="请选择项目类型">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.TTS_PROJECT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申请部门" prop="deptId">
<el-tree-select
disabled
v-model="formData.deptId"
:data="deptTree"
:props="defaultProps"
check-strictly
node-key="id"
placeholder=""
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申请人" prop="userId">
<el-select v-model="formData.userId" disabled placeholder="请选择申请人">
<el-option
v-for="dict in userOptions"
:key="dict.id"
:label="dict.nickname"
:value="dict.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="测试完成日期" prop="testCompletionDate">
<el-date-picker
disabled
v-model="formData.testCompletionDate"
style="width: 100%"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择测试完成日期"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申请上线日期" prop="onlineDate">
<el-date-picker
v-model="formData.onlineDate"
style="width: 100%"
disabled
type="date"
value-format="YYYY-MM-DD"
placeholder="选择申请上线日期"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="涉及第三方系统" prop="externalSystem">
<el-radio-group v-model="formData.externalSystem" disabled>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<OnlineApplicationList ref="onlineApplicationFormRef" :type="1" :onlineModuleInfo="formData.onlineModuleInfo" />
</el-col>
<el-col :span="24">
<el-form-item label="上线注意事项" prop="precautions">
<el-input type="textarea" disabled :rows="5" v-model="formData.precautions" placeholder="请输入上线注意事项" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="失败应急处理" prop="emergencyManagement">
<el-input type="textarea" disabled :rows="5" v-model="formData.emergencyManagement" placeholder="请输入失败应急处理" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="申请人签字" prop="applicantSign">
<div class="signature-preview">
<el-image class="image" v-if="formData.applicantSign" :src="formData.applicantSign" alt="签名预览" />
</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item v-if="visible" >
<div>
<HandleSign @success="setSign" @close="visible = false" />
</div>
</el-form-item>
<!-- <el-form-item label="工作流编号" prop="processInstanceId">
<el-input v-model="formData.processInstanceId" placeholder="请输入工作流编号" />
</el-form-item>
<el-form-item label="审批状态" prop="auditStatus">
<el-radio-group v-model="formData.auditStatus">
<el-radio value="1">请选择字典生成</el-radio>
</el-radio-group>
</el-form-item> -->
</el-form>
</ContentWrap>
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { OnlineApplicationApi, OnlineApplicationVO } from '@/api/task/online'
import HandleSign from './HandleSign.vue'
import OnlineApplicationList from './components/index.vue'
import { defaultProps, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
import { propTypes } from '@/utils/propTypes'
import * as UserApi from '@/api/system/user'
/** 上线申请 表单 */
defineOptions({ name: 'OnlineApplicationForm' })
const { proxy }: any = getCurrentInstance();
const userOptions = ref<UserApi.UserVO[]>([]) //
const { t } = useI18n() //
const message = useMessage() //
const deptTree = ref() //
const props = defineProps({
id: propTypes.number.def(undefined),
type: propTypes.number.def(undefined),
})
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const visible = ref(false)
const onlineApplicationFormRef = ref('')
const formData = ref({
id: undefined,
projectName: undefined,
projectType: undefined,
deptId: undefined,
userId: undefined,
testCompletionDate: undefined,
onlineDate: undefined,
externalSystem: undefined,
precautions: undefined,
emergencyManagement: undefined,
applicantSign: undefined,
processInstanceId: undefined,
auditStatus: undefined
})
const formRules = reactive({
projectName: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
projectType: [{ required: true, message: '项目类型不能为空', trigger: 'change' }],
deptId: [{ required: true, message: '申请部门不能为空', trigger: 'blur' }],
userId: [{ required: true, message: '申请人不能为空', trigger: 'blur' }],
testCompletionDate: [{ required: true, message: '测试完成日期不能为空', trigger: 'blur' }],
onlineDate: [{ required: true, message: '申请上线日期不能为空', trigger: 'blur' }],
externalSystem: [{ required: true, message: '上线系统是否涉及第三方系统不能为空', trigger: 'blur' }],
precautions: [{ required: true, message: '上线注意事项不能为空', trigger: 'blur' }],
emergencyManagement: [{ required: true, message: '失败应急处理不能为空', trigger: 'blur' }],
applicantSign: [{ required: true, message: '申请人签字不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (id?: number) => {
dialogVisible.value = true
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await OnlineApplicationApi.getOnlineApplication(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
const setSign = (val) => {
console.log('%csrc/views/task/online/OnlineApplicationForm.vue:143 val', 'color: #007acc;', val);
formData.value.applicantSign = val
visible.value = false
}
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as OnlineApplicationVO
// await onlineApplicationFormRef.value.validate()
console.log('%csrc/views/task/online/OnlineApplicationForm.vue:214 onlineApplicationFormRef.value.getData()', 'color: #007acc;', onlineApplicationFormRef.value.getData());
data.onlineModuleInfo = onlineApplicationFormRef.value.getData()
if (!formType.value) {
await OnlineApplicationApi.createOnlineApplication(data)
message.success(t('common.createSuccess'))
} else {
await OnlineApplicationApi.updateOnlineApplication(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
goBack()
//
emit('success')
} finally {
formLoading.value = false
}
}
const goBack = ()=> {
proxy.$router.go(-1)
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
projectName: undefined,
projectType: undefined,
deptId: undefined,
userId: undefined,
testCompletionDate: undefined,
onlineDate: undefined,
externalSystem: undefined,
precautions: undefined,
emergencyManagement: undefined,
applicantSign: undefined,
processInstanceId: undefined,
auditStatus: undefined
}
formRef.value?.resetFields()
}
const route = useRoute();
onMounted(async()=> {
formType.value = props.id || route.query.id
if (formType.value) await open(formType.value)
deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
//
userOptions.value = await UserApi.getSimpleUserList()
})
</script>
<style scoped>
.signature-preview {
display: flex;
flex-direction: column;
}
.image {
width: 200px;
margin-top: 5px;
}
</style>

View File

@ -5,56 +5,109 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
label-width="130px"
v-loading="formLoading"
>
<el-form-item label="产品名称" prop="projectName">
<el-input v-model="formData.projectName" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="项目类型" prop="projectType">
<el-select v-model="formData.projectType" placeholder="请选择项目类型">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.TTS_PROJECT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
<el-row>
<el-col :span="8">
<el-form-item label="产品名称" prop="projectName">
<el-input v-model="formData.projectName" placeholder="请输入产品名称" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目类型" prop="projectType">
<el-select v-model="formData.projectType" placeholder="请选择项目类型">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.TTS_PROJECT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申请部门" prop="deptId">
<el-tree-select
v-model="formData.deptId"
:data="deptTree"
:props="defaultProps"
check-strictly
node-key="id"
placeholder="请选择申请部门"
/>
</el-select>
</el-form-item>
<el-form-item label="申请部门" prop="deptId">
<el-input v-model="formData.deptId" placeholder="请输入申请部门" />
</el-form-item>
<el-form-item label="申请人" prop="userId">
<el-input v-model="formData.userId" placeholder="请输入申请人" />
</el-form-item>
<el-form-item label="测试完成日期" prop="testCompletionDate">
<el-date-picker
v-model="formData.testCompletionDate"
type="date"
value-format="x"
placeholder="选择测试完成日期"
/>
</el-form-item>
<el-form-item label="申请上线日期" prop="onlineDate">
<el-date-picker
v-model="formData.onlineDate"
type="date"
value-format="x"
placeholder="选择申请上线日期"
/>
</el-form-item>
<el-form-item label="上线系统是否涉及第三方系统" prop="externalSystem">
<el-input v-model="formData.externalSystem" placeholder="请输入上线系统是否涉及第三方系统(如:是/否)" />
</el-form-item>
<el-form-item label="上线注意事项" prop="precautions">
<el-input v-model="formData.precautions" placeholder="请输入上线注意事项" />
</el-form-item>
<el-form-item label="失败应急处理" prop="emergencyManagement">
<el-input v-model="formData.emergencyManagement" placeholder="请输入失败应急处理" />
</el-form-item>
<el-form-item label="申请人签字" prop="applicantSign">
<Ceshi />
</el-form-item>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申请人" prop="userId">
<el-select v-model="formData.userId" placeholder="请选择申请人">
<el-option
v-for="dict in userOptions"
:key="dict.id"
:label="dict.nickname"
:value="dict.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="测试完成日期" prop="testCompletionDate">
<el-date-picker
v-model="formData.testCompletionDate"
style="width: 100%"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择测试完成日期"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="申请上线日期" prop="onlineDate">
<el-date-picker
v-model="formData.onlineDate"
style="width: 100%"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择申请上线日期"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="涉及第三方系统" prop="externalSystem">
<el-radio-group v-model="formData.externalSystem">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<OnlineApplicationList ref="onlineApplicationFormRef" :onlineModuleInfo="formData.onlineModuleInfo" />
</el-col>
<el-col :span="24">
<el-form-item label="上线注意事项" prop="precautions">
<el-input type="textarea" :rows="5" v-model="formData.precautions" placeholder="请输入上线注意事项" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="失败应急处理" prop="emergencyManagement">
<el-input type="textarea" :rows="5" v-model="formData.emergencyManagement" placeholder="请输入失败应急处理" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="申请人签字" prop="applicantSign">
<div class="signature-preview">
<el-button @click="visible = true">{{formData.applicantSign ? '重新签名' : '点击签名'}}</el-button>
<el-image class="image" v-if="formData.applicantSign" :src="formData.applicantSign" alt="签名预览" />
</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item v-if="visible" >
<div>
<HandleSign @success="setSign" @close="visible = false" />
</div>
</el-form-item>
<!-- <el-form-item label="工作流编号" prop="processInstanceId">
<el-input v-model="formData.processInstanceId" placeholder="请输入工作流编号" />
</el-form-item>
@ -65,27 +118,40 @@
</el-form-item> -->
</el-form>
<template #footer>
<div style="text-align: right">
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
<el-button @click="goBack"> </el-button>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { OnlineApplicationApi, OnlineApplicationVO } from '@/api/task/online'
import Ceshi from './ceshi.vue'
import HandleSign from './HandleSign.vue'
import OnlineApplicationList from './components/index.vue'
import { defaultProps, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
import { propTypes } from '@/utils/propTypes'
import * as UserApi from '@/api/system/user'
/** 上线申请 表单 */
defineOptions({ name: 'OnlineApplicationForm' })
const { proxy }: any = getCurrentInstance();
const userOptions = ref<UserApi.UserVO[]>([]) //
const { t } = useI18n() //
const message = useMessage() //
const deptTree = ref() //
const props = defineProps({
id: propTypes.number.def(undefined),
type: propTypes.number.def(undefined),
})
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const visible = ref(false)
const onlineApplicationFormRef = ref('')
const formData = ref({
id: undefined,
projectName: undefined,
@ -108,7 +174,7 @@ const formRules = reactive({
userId: [{ required: true, message: '申请人不能为空', trigger: 'blur' }],
testCompletionDate: [{ required: true, message: '测试完成日期不能为空', trigger: 'blur' }],
onlineDate: [{ required: true, message: '申请上线日期不能为空', trigger: 'blur' }],
externalSystem: [{ required: true, message: '上线系统是否涉及第三方系统(如:是/否)不能为空', trigger: 'blur' }],
externalSystem: [{ required: true, message: '上线系统是否涉及第三方系统不能为空', trigger: 'blur' }],
precautions: [{ required: true, message: '上线注意事项不能为空', trigger: 'blur' }],
emergencyManagement: [{ required: true, message: '失败应急处理不能为空', trigger: 'blur' }],
applicantSign: [{ required: true, message: '申请人签字不能为空', trigger: 'blur' }]
@ -116,10 +182,8 @@ const formRules = reactive({
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
const open = async (id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
@ -133,6 +197,12 @@ const open = async (type: string, id?: number) => {
}
defineExpose({ open }) // open
const setSign = (val) => {
console.log('%csrc/views/task/online/OnlineApplicationForm.vue:143 val', 'color: #007acc;', val);
formData.value.applicantSign = val
visible.value = false
}
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
@ -142,7 +212,10 @@ const submitForm = async () => {
formLoading.value = true
try {
const data = formData.value as unknown as OnlineApplicationVO
if (formType.value === 'create') {
// await onlineApplicationFormRef.value.validate()
console.log('%csrc/views/task/online/OnlineApplicationForm.vue:214 onlineApplicationFormRef.value.getData()', 'color: #007acc;', onlineApplicationFormRef.value.getData());
data.onlineModuleInfo = onlineApplicationFormRef.value.getData()
if (!formType.value) {
await OnlineApplicationApi.createOnlineApplication(data)
message.success(t('common.createSuccess'))
} else {
@ -150,6 +223,8 @@ const submitForm = async () => {
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
goBack()
//
emit('success')
} finally {
@ -157,6 +232,10 @@ const submitForm = async () => {
}
}
const goBack = ()=> {
proxy.$router.go(-1)
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
@ -176,4 +255,25 @@ const resetForm = () => {
}
formRef.value?.resetFields()
}
</script>
const route = useRoute();
onMounted(async()=> {
formType.value = props.id || route.query.id
if (formType.value) await open(formType.value)
deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
//
userOptions.value = await UserApi.getSimpleUserList()
})
</script>
<style scoped>
.signature-preview {
display: flex;
flex-direction: column;
}
.image {
width: 200px;
margin-top: 5px;
}
</style>

View File

@ -1,197 +0,0 @@
<template>
<div class="handwritten-name-wrap">
<el-button plain @click="handleClick">
手写签名
</el-button>
<div class="img-wrap">
<el-image :src="imgSrc" alt="" v-if="imgSrc" />
</div>
<vxe-modal
v-model="panelVisible"
title="手写签名"
width="600"
height="400"
size="large"
:destroy-on-close="true"
class="signNameModel"
>
<div class="signWrap">
<VueSignaturePad
width="100%"
height="100%"
ref="signaturePad"
:options="options"
/>
<footer>
<div class="gtnGroup">
<el-button type="primary" size="mini" @click="undo"></el-button>
<el-button type="primary" size="mini" style="margin-left:20px" @click="clear"></el-button>
<el-button type="primary" size="mini" style="margin-left:20px" @click="save"></el-button>
</div>
<div class="otherSet">
<div class="penTxt">笔刷大小</div>
<div class="circleWrap" :class="{ active: isActive1 }" @click="selSize(1)">
<b class="b1"></b>
</div>
<div class="circleWrap" :class="{ active: isActive2 }" @click="selSize(2)">
<b class="b2"></b>
</div>
<div class="circleWrap" :class="{ active: isActive3 }" @click="selSize(3)">
<b class="b3"></b>
</div>
</div>
</footer>
</div>
</vxe-modal>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// import VueSignaturePad from 'vue-signature-pad';
const panelVisible = ref(false);
const imgSrc = ref('');
const signaturePad = ref(null);
const isActive1 = ref(true);
const isActive2 = ref(false);
const isActive3 = ref(false);
const options = ref({
penColor: '#000',
minWidth: 1,
maxWidth: 1,
});
const handleClick = () => {
panelVisible.value = true;
isActive1.value = true;
isActive2.value = false;
isActive3.value = false;
options.value = {
penColor: '#000',
minWidth: 1,
maxWidth: 1,
};
};
const undo = () => {
signaturePad.value.undoSignature();
};
const clear = () => {
signaturePad.value.clearSignature();
};
const save = () => {
const { isEmpty, data } = signaturePad.value.saveSignature();
imgSrc.value = data;
panelVisible.value = false;
};
const selSize = (val: number) => {
options.value = {
penColor: '#000',
minWidth: val,
maxWidth: val,
};
if (val === 1) {
isActive1.value = true;
isActive2.value = false;
isActive3.value = false;
} else if (val === 2) {
isActive1.value = false;
isActive2.value = true;
isActive3.value = false;
} else if (val === 3) {
isActive1.value = false;
isActive2.value = false;
isActive3.value = true;
}
};
</script>
<style lang="scss">
.handwritten-name-wrap {
.img-wrap {
width: 100%;
height: 164px;
margin-top: 2px;
border: 1px solid #ccc;
img {
width: 70%;
height: 100%;
}
}
.signWrap {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
.signName {
flex: 1;
border-top: 1px solid #ccc;
}
footer {
height: 40px;
border-top: 1px solid #ccc;
display: flex;
justify-content: space-between;
align-items: center;
.gtnGroup {
width: 50%;
margin-left: 20px;
}
.otherSet {
width: 50%;
display: flex;
align-items: center;
.penTxt {
width: 70px;
}
.selSize {
width: 70px;
}
.el-select__caret {
position: absolute;
right: -3px;
}
.b1, .b2, .b3 {
display: inline-block;
background: #000;
border-radius: 50%;
}
.circleWrap {
display: flex;
justify-content: center;
align-items: center;
width: 18px;
height: 18px;
cursor: pointer;
margin-right: 20px;
}
.active {
border: 1px dashed #0074d9;
}
.b1 {
width: 4px;
height: 4px;
}
.b2 {
width: 6px;
height: 6px;
}
.b3 {
width: 8px;
height: 8px;
}
}
}
}
}
.signNameModel {
.vxe-modal--content {
padding: 0 !important;
}
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="上线申请ID" prop="onlineApplicationId">
<el-input v-model="formData.onlineApplicationId" placeholder="请输入上线申请ID" />
</el-form-item>
<el-form-item label="涉及模块" prop="involvingModule">
<el-input v-model="formData.involvingModule" placeholder="请输入涉及模块" />
</el-form-item>
<el-form-item label="版本" prop="version">
<el-input v-model="formData.version" placeholder="请输入版本" />
</el-form-item>
<el-form-item label="模块说明" prop="moduleDescription">
<Editor v-model="formData.moduleDescription" height="150px" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { OnlineModuleInfoApi, OnlineModuleInfoVO } from '@/api/task/onlineModule'
/** 上线模块信息 表单 */
defineOptions({ name: 'OnlineModuleInfoForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
onlineApplicationId: undefined,
involvingModule: undefined,
version: undefined,
moduleDescription: undefined
})
const formRules = reactive({
onlineApplicationId: [{ required: true, message: '上线申请ID不能为空', trigger: 'blur' }],
involvingModule: [{ required: true, message: '涉及模块不能为空', trigger: 'blur' }],
version: [{ required: true, message: '版本不能为空', trigger: 'blur' }],
moduleDescription: [{ required: true, message: '模块说明不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await OnlineModuleInfoApi.getOnlineModuleInfo(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as OnlineModuleInfoVO
if (formType.value === 'create') {
await OnlineModuleInfoApi.createOnlineModuleInfo(data)
message.success(t('common.createSuccess'))
} else {
await OnlineModuleInfoApi.updateOnlineModuleInfo(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
onlineApplicationId: undefined,
involvingModule: undefined,
version: undefined,
moduleDescription: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,216 @@
<template>
<!-- 列表 -->
<ContentWrap>
<el-form
ref="contactRef"
:model="formData"
:rules="formRules"
v-loading="formLoading"
label-width="0px"
:inline-message="true"
>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="涉及模块" align="center">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.involvingModule`" :rules="formRules.involvingModule" class="mb-0px!" v-if="!type">
<el-input v-model="row.involvingModule" placeholder="请输入涉及模块" />
</el-form-item>
<div v-else>
{{row.involvingModule}}
</div>
</template>
</el-table-column>
<el-table-column label="版本" align="center">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.version`" :rules="formRules.version" class="mb-0px!" v-if="!type">
<el-input v-model="row.version" placeholder="请输入版本" />
</el-form-item>
<div v-else>
{{row.version}}
</div>
</template>
</el-table-column>
<el-table-column label="模块说明" align="center">
<template #default="{ row, $index }">
<el-form-item :prop="`${$index}.moduleDescription`" :rules="formRules.moduleDescription" class="mb-0px!" v-if="!type">
<el-input v-model="row.moduleDescription" placeholder="请输入模块说明" />
</el-form-item>
<div v-else>
{{row.moduleDescription}}
</div>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120px" v-if="!type">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['tts:online-module-info:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['tts:online-module-info:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<el-row justify="center" class="mt-3" v-if="!type">
<el-button @click="handleAdd" round>+ 添加</el-button>
</el-row>
<!-- 分页 -->
<!-- <Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/> -->
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<OnlineModuleInfoForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { OnlineModuleInfoApi, OnlineModuleInfoVO } from '@/api/task/onlineModule'
import OnlineModuleInfoForm from './OnlineModuleInfoForm.vue'
import { propTypes } from '@/utils/propTypes'
/** 上线模块信息 列表 */
defineOptions({ name: 'OnlineModuleInfo' })
const message = useMessage() //
const { t } = useI18n() //
const props = defineProps({
onlineModuleInfo: [],
type: propTypes.number.def(undefined)
})
const loading = ref(false) //
const list = ref<OnlineModuleInfoVO[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
onlineApplicationId: undefined,
involvingModule: undefined,
version: undefined,
moduleDescription: undefined,
createTime: []
})
const formRules = reactive({
involvingModule: [{ required: true, message: '涉及模块不能为空', trigger: 'blur' }],
version: [{ required: true, message: '版本不能为空', trigger: 'blur' }],
onlineApplicationId: [{ required: true, message: '模块说明不能为空', trigger: 'blur' }],
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
watch(
() => props.onlineModuleInfo,
async (val) => {
if (!val || val.length === 0) {
return;
}
await nextTick(() => {
list.value = val
})
},
{ immediate: true },
);
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await OnlineModuleInfoApi.getOnlineModuleInfoPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const contactRef = ref() // Ref
/** 新增按钮操作 */
const handleAdd = () => {
const row = {
moduleDescription: undefined,
version: undefined,
involvingModule: undefined,
}
list.value.push(row)
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await OnlineModuleInfoApi.deleteOnlineModuleInfo(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await OnlineModuleInfoApi.exportOnlineModuleInfo(queryParams)
download.excel(data, '上线模块信息.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 表单校验 */
const validate = () => {
return formRef.value.validate()
}
const getData = (data) => {
console.log('%csrc/views/task/online/components/index.vue:175 list.value', 'color: #007acc;', list.value);
return list.value
}
defineExpose({ validate, getData })
/** 初始化 **/
onMounted(() => {
// getList()
})
</script>

View File

@ -18,18 +18,20 @@
/>
</el-form-item>
<el-form-item label="申请部门" prop="deptId">
<el-input
v-model="queryParams.deptId"
placeholder="请输入申请部门"
clearable
@keyup.enter="handleQuery"
<el-tree-select
v-model="queryParams.deptId"
:data="deptTree"
:props="defaultProps"
check-strictly
class="!w-240px"
/>
node-key="id"
placeholder="请选择申请部门"
/>
</el-form-item>
<el-form-item label="申请上线日期" prop="createTime">
<el-form-item label="申请上线日期" prop="onlineDate">
<el-date-picker
v-model="queryParams.createTime"
v-model="queryParams.onlineDate"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
@ -73,16 +75,33 @@
<dict-tag :type="DICT_TYPE.TTS_PROJECT_TYPE" :value="scope.row.projectType" />
</template>
</el-table-column> -->
<el-table-column label="申请人" align="center" prop="userId" />
<el-table-column label="申请部门" align="center" prop="deptId" />
<el-table-column label="申请人" align="center" prop="userId">
<template #default="scope">
{{getName(userOptions, scope.row.userId, 'id', 'nickname')}}
</template>
</el-table-column>
<el-table-column label="申请部门" align="center" prop="deptId">
<template #default="scope">
{{getName(deptTreeList, scope.row.deptId, 'id', 'name')}}
</template>
</el-table-column>
<el-table-column label="测试完成日期" align="center" prop="testCompletionDate" />
<el-table-column label="申请上线日期" align="center" prop="onlineDate" />
<el-table-column label="上线系统是否涉及第三方系统" align="center" prop="externalSystem" />
<el-table-column label="上线系统是否涉及第三方系统" align="center" prop="externalSystem" >
<template #default="scope">
{{scope.row.externalSystem ? '是' : '否'}}
</template>
</el-table-column>
<el-table-column label="审批状态" align="center" prop="auditStatus" width="110">
<template #default="scope">
<dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
</template>
</el-table-column>
<!-- <el-table-column label="上线注意事项" align="center" prop="precautions" />
<el-table-column label="失败应急处理" align="center" prop="emergencyManagement" />
<el-table-column label="申请人签字" align="center" prop="applicantSign" />
<el-table-column label="工作流编号" align="center" prop="processInstanceId" />
<el-table-column label="审批状态" align="center" prop="auditStatus" />
<el-table-column label="创建者" align="center" prop="creator" /> -->
<!-- <el-table-column
label="创建时间"
@ -99,24 +118,32 @@
:formatter="dateFormatter"
width="180px"
/> -->
<el-table-column label="操作" align="center" min-width="120px">
<el-table-column label="操作" fixed="right" align="center" min-width="120px">
<template #default="scope">
<el-button
<!-- <el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
@click="openFormEdit(scope.row)"
v-hasPermi="['tts:online-application:update']"
>
编辑
</el-button>
</el-button> -->
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['tts:online-application:delete']"
>
删除
</el-button>
v-if="scope.row.auditStatus !== 0"
link
type="primary"
@click="handleProcessDetail(scope.row)"
>
进度
</el-button>
<el-button
link
type="primary"
@click="openFormDetail(scope.row)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
@ -130,7 +157,7 @@
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<OnlineApplicationForm ref="formRef" @success="getList" />
<!-- <OnlineApplicationForm ref="formRef" @success="getList" /> -->
</template>
<script setup lang="ts">
@ -138,13 +165,18 @@ import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { OnlineApplicationApi, OnlineApplicationVO } from '@/api/task/online'
import OnlineApplicationForm from './OnlineApplicationForm.vue'
import * as DeptApi from '@/api/system/dept'
import { defaultProps, handleTree } from '@/utils/tree'
import * as UserApi from '@/api/system/user'
/** 上线申请 列表 */
defineOptions({ name: 'OnlineApplication' })
const userOptions = ref<UserApi.UserVO[]>([]) //
const message = useMessage() //
const { t } = useI18n() //
const deptTree = ref() //
const deptTreeList = ref() //
const loading = ref(true) //
const list = ref<OnlineApplicationVO[]>([]) //
@ -203,10 +235,25 @@ const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
//
const openFormAdd = () => {
push({ name: 'OnlineAppAdd' })
}
//
const openFormEdit = (row: Object) => {
router.push({ name: 'OnlineAppEdit', query: { id: row.id} })
}
const openFormDetail = (row: Object) => {
router.push({ name: 'OnlineAppDetail', query: { id: row.id} })
}
/** 查看审批 */
const handleProcessDetail = (row) => {
router.push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
@ -235,8 +282,18 @@ const handleExport = async () => {
}
}
const getName = (opt, val, id, name) => {
const arr = opt.filter(v => v[id] == val)
return arr.length ? arr[0][name] : ''
}
/** 初始化 **/
onMounted(() => {
onMounted(async() => {
deptTreeList.value = await DeptApi.getSimpleDeptList()
deptTree.value = handleTree(deptTreeList.value)
//
userOptions.value = await UserApi.getSimpleUserList()
getList()
})
</script>