This commit is contained in:
吴红兵
2026-03-05 15:00:19 +08:00
parent bff8907f88
commit 2317909261
9 changed files with 397 additions and 348 deletions

View File

@@ -8,6 +8,7 @@
Vue 3 SPA with custom composables for data fetching. Element Plus for UI.
## Structure
```
cloud-ui/src/
├── api/ # API modules (one per entity)
@@ -24,7 +25,7 @@ cloud-ui/src/
## Key Hooks
| Hook | Purpose |
|------|---------|
| ----------------------------------- | ---------------------------- |
| `useDict(type1, type2)` | Dictionary data with caching |
| `useTable({ pageList, queryForm })` | Table + pagination |
| `useMessage()` | Toast notifications |
@@ -32,6 +33,7 @@ cloud-ui/src/
| `useParam(type)` | System parameters |
## API Pattern
```typescript
import request from '/@/utils/request';
@@ -43,6 +45,7 @@ export const delObj = (id) => request.post('/module/entity/delete', id);
```
## Hooks Usage
```typescript
// Dictionary
const { dict } = useDict('sex', 'status');
@@ -51,7 +54,7 @@ const { dict } = useDict('sex', 'status');
// Table
const { tableData, loading, getData, pagination } = useTable({
pageList: fetchList,
queryForm: reactive({ name: '' })
queryForm: reactive({ name: '' }),
});
// Message
@@ -66,7 +69,7 @@ await msgBoxConfirm('确定删除?');
## Constraints
| Rule | Details |
|------|---------|
| --------------- | --------------------------------------------- |
| **Hardcoding** | NO hardcoded strings. Define constants first. |
| **Line width** | 150 chars (Prettier) |
| **Indentation** | Tabs (Prettier) |
@@ -75,6 +78,7 @@ await msgBoxConfirm('确定删除?');
| **API deletes** | POST /delete (NOT DELETE) |
## Commands
```bash
npm run dev # Development server
npm run build # Production build
@@ -85,7 +89,7 @@ npm run prettier # Format code
## Non-Standard
| Location | Issue |
|----------|-------|
| ------------------------------------ | ----------------------------- |
| `src/composables/` + `src/hooks/` | Both exist (prefer hooks/) |
| `src/directive/` + `src/directives/` | Both exist |
| `src/const/` | Named "const" not "constants" |
@@ -93,6 +97,7 @@ npm run prettier # Format code
## ESLint Relaxed Rules
Many TypeScript rules are OFF:
- `@typescript-eslint/no-explicit-any`: OFF
- `@typescript-eslint/explicit-function-return-type`: OFF
- `vue/require-default-prop`: OFF

View File

@@ -3,12 +3,14 @@
## 方案 1TableColumn 包装组件 ⭐ 推荐
### 优点
- ✅ 使用简单,只需替换组件名
- ✅ 完全兼容 `el-table-column` 的所有属性和插槽
- ✅ 代码清晰,易于维护
- ✅ 无需修改现有代码结构
### 缺点
- ❌ 需要创建一个新组件
- ❌ 需要在使用的地方 provide `isColumnVisible` 函数
@@ -35,12 +37,12 @@
</template>
<script setup>
import TableColumnProvider from '/@/components/TableColumn/Provider.vue'
import TableColumn from '/@/components/TableColumn/index.vue'
import TableColumnProvider from '/@/components/TableColumn/Provider.vue';
import TableColumn from '/@/components/TableColumn/index.vue';
const isColumnVisible = (propOrLabel) => {
return tableColumnControlRef.value?.isColumnVisible?.(propOrLabel) ?? true
}
return tableColumnControlRef.value?.isColumnVisible?.(propOrLabel) ?? true;
};
</script>
```
@@ -49,10 +51,12 @@ const isColumnVisible = (propOrLabel) => {
## 方案 2自定义指令 v-column-visible
### 优点
- ✅ 可以保留 `el-table-column`
- ✅ 使用相对简单
### 缺点
- ❌ 仍然需要在每个列上添加指令
- ❌ 实现较复杂,需要操作 DOM
- ❌ 可能不够优雅
@@ -62,12 +66,7 @@ const isColumnVisible = (propOrLabel) => {
```vue
<template>
<el-table>
<el-table-column
v-column-visible="'tied'"
prop="tied"
label="是否退休"
width="100"
/>
<el-table-column v-column-visible="'tied'" prop="tied" label="是否退休" width="100" />
</el-table>
</template>
```
@@ -77,10 +76,12 @@ const isColumnVisible = (propOrLabel) => {
## 方案 3使用 computed 动态生成列配置
### 优点
- ✅ 最彻底,完全控制列的渲染
- ✅ 性能最好(只渲染可见的列)
### 缺点
- ❌ 需要重构现有代码,改动较大
- ❌ 需要将列配置抽离出来
- ❌ 插槽处理较复杂
@@ -90,11 +91,7 @@ const isColumnVisible = (propOrLabel) => {
```vue
<template>
<el-table>
<component
v-for="col in visibleColumns"
:is="col.component"
v-bind="col.props"
>
<component v-for="col in visibleColumns" :is="col.component" v-bind="col.props">
<template v-if="col.slots" #[slotName]="slotProps" v-for="(slot, slotName) in col.slots">
<component :is="slot" v-bind="slotProps" />
</template>
@@ -106,11 +103,11 @@ const isColumnVisible = (propOrLabel) => {
const columns = [
{ prop: 'tied', label: '是否退休', component: 'el-table-column', slots: { default: StatusTag } },
// ...
]
];
const visibleColumns = computed(() => {
return columns.filter(col => isColumnVisible(col.prop || col.label))
})
return columns.filter((col) => isColumnVisible(col.prop || col.label));
});
</script>
```
@@ -119,10 +116,12 @@ const visibleColumns = computed(() => {
## 方案 4在 el-table 层面使用 provide + 自动处理
### 优点
- ✅ 在表格层面统一处理
- ✅ 子组件自动继承
### 缺点
- ❌ 实现最复杂
- ❌ 需要修改 Element Plus 的渲染逻辑
- ❌ 可能影响性能
@@ -132,6 +131,7 @@ const visibleColumns = computed(() => {
## 推荐方案
**推荐使用方案 1TableColumn 包装组件)**,因为:
1. 使用最简单,只需替换组件名
2. 完全兼容现有代码
3. 易于维护和理解
@@ -140,12 +140,13 @@ const visibleColumns = computed(() => {
## 迁移步骤(方案 1
1. 导入组件:
```vue
import TableColumnProvider from '/@/components/TableColumn/Provider.vue'
import TableColumn from '/@/components/TableColumn/index.vue'
import TableColumnProvider from '/@/components/TableColumn/Provider.vue' import TableColumn from '/@/components/TableColumn/index.vue'
```
2.`TableColumnProvider` 包裹所有列,并传入 `isColumnVisible`
```vue
<TableColumnProvider :is-column-visible="isColumnVisible">
<!-- 所有列 -->
@@ -153,6 +154,7 @@ import TableColumn from '/@/components/TableColumn/index.vue'
```
3. 将所有 `el-table-column` 替换为 `TableColumn`,并移除 `v-if`
```vue
<!-- 之前 -->
<el-table-column v-if="isColumnVisible('tied')" prop="tied" label="是否退休" />
@@ -160,4 +162,3 @@ import TableColumn from '/@/components/TableColumn/index.vue'
<!-- 之后 -->
<TableColumn prop="tied" label="是否退休" />
```

140
auto-imports.d.ts vendored
View File

@@ -1,73 +1,73 @@
// Generated by 'unplugin-auto-import'
export {}
export {};
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveDirective: typeof import('vue')['resolveDirective']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useLink: typeof import('vue-router')['useLink']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const EffectScope: typeof import('vue')['EffectScope'];
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'];
const computed: typeof import('vue')['computed'];
const createApp: typeof import('vue')['createApp'];
const createPinia: typeof import('pinia')['createPinia'];
const customRef: typeof import('vue')['customRef'];
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'];
const defineComponent: typeof import('vue')['defineComponent'];
const defineStore: typeof import('pinia')['defineStore'];
const effectScope: typeof import('vue')['effectScope'];
const getActivePinia: typeof import('pinia')['getActivePinia'];
const getCurrentInstance: typeof import('vue')['getCurrentInstance'];
const getCurrentScope: typeof import('vue')['getCurrentScope'];
const h: typeof import('vue')['h'];
const inject: typeof import('vue')['inject'];
const isProxy: typeof import('vue')['isProxy'];
const isReactive: typeof import('vue')['isReactive'];
const isReadonly: typeof import('vue')['isReadonly'];
const isRef: typeof import('vue')['isRef'];
const mapActions: typeof import('pinia')['mapActions'];
const mapGetters: typeof import('pinia')['mapGetters'];
const mapState: typeof import('pinia')['mapState'];
const mapStores: typeof import('pinia')['mapStores'];
const mapWritableState: typeof import('pinia')['mapWritableState'];
const markRaw: typeof import('vue')['markRaw'];
const nextTick: typeof import('vue')['nextTick'];
const onActivated: typeof import('vue')['onActivated'];
const onBeforeMount: typeof import('vue')['onBeforeMount'];
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'];
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'];
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'];
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'];
const onDeactivated: typeof import('vue')['onDeactivated'];
const onErrorCaptured: typeof import('vue')['onErrorCaptured'];
const onMounted: typeof import('vue')['onMounted'];
const onRenderTracked: typeof import('vue')['onRenderTracked'];
const onRenderTriggered: typeof import('vue')['onRenderTriggered'];
const onScopeDispose: typeof import('vue')['onScopeDispose'];
const onServerPrefetch: typeof import('vue')['onServerPrefetch'];
const onUnmounted: typeof import('vue')['onUnmounted'];
const onUpdated: typeof import('vue')['onUpdated'];
const provide: typeof import('vue')['provide'];
const reactive: typeof import('vue')['reactive'];
const readonly: typeof import('vue')['readonly'];
const ref: typeof import('vue')['ref'];
const resolveComponent: typeof import('vue')['resolveComponent'];
const resolveDirective: typeof import('vue')['resolveDirective'];
const setActivePinia: typeof import('pinia')['setActivePinia'];
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'];
const shallowReactive: typeof import('vue')['shallowReactive'];
const shallowReadonly: typeof import('vue')['shallowReadonly'];
const shallowRef: typeof import('vue')['shallowRef'];
const storeToRefs: typeof import('pinia')['storeToRefs'];
const toRaw: typeof import('vue')['toRaw'];
const toRef: typeof import('vue')['toRef'];
const toRefs: typeof import('vue')['toRefs'];
const triggerRef: typeof import('vue')['triggerRef'];
const unref: typeof import('vue')['unref'];
const useAttrs: typeof import('vue')['useAttrs'];
const useCssModule: typeof import('vue')['useCssModule'];
const useCssVars: typeof import('vue')['useCssVars'];
const useLink: typeof import('vue-router')['useLink'];
const useRoute: typeof import('vue-router')['useRoute'];
const useRouter: typeof import('vue-router')['useRouter'];
const useSlots: typeof import('vue')['useSlots'];
const watch: typeof import('vue')['watch'];
const watchEffect: typeof import('vue')['watchEffect'];
const watchPostEffect: typeof import('vue')['watchPostEffect'];
const watchSyncEffect: typeof import('vue')['watchSyncEffect'];
}

View File

@@ -1,26 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta charset="UTF-8" />
<title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>
</head>
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"
offset="0">
<table width="95%" cellpadding="0" cellspacing="0"
style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0">
<table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
<tr>
<td>(本邮件是程序自动下发的,请勿回复!)</td>
</tr>
<tr>
<td><h2>
<td>
<h2>
<font color="#FF0000">构建结果 - ${BUILD_STATUS}</font>
</h2></td>
</h2>
</td>
</tr>
<tr>
<td><br />
<td>
<br />
<b><font color="#0B610B">构建信息</font></b>
<hr size="2" width="100%" align="center" /></td>
<hr size="2" width="100%" align="center" />
</td>
</tr>
<tr>
<td>
@@ -36,41 +38,51 @@
</td>
</tr>
<tr>
<td><b><font color="#0B610B">Changes Since Last
Successful Build:</font></b>
<hr size="2" width="100%" align="center" /></td>
<td>
<b><font color="#0B610B">Changes Since Last Successful Build:</font></b>
<hr size="2" width="100%" align="center" />
</td>
</tr>
<tr>
<td>
<ul>
<li>历史变更记录 : <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
</ul> ${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for Build #%n:<br />%c<br />",showPaths=true,changesFormat="<pre>[%a]<br />%m</pre>",pathFormat="&nbsp;&nbsp;&nbsp;&nbsp;%p"}
</ul>
${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for Build #%n:<br />%c<br />",showPaths=true,changesFormat="
<pre>[%a]<br />%m</pre>
",pathFormat="&nbsp;&nbsp;&nbsp;&nbsp;%p"}
</td>
</tr>
<tr>
<td><b>Failed Test Results</b>
<hr size="2" width="100%" align="center" /></td>
<td>
<b>Failed Test Results</b>
<hr size="2" width="100%" align="center" />
</td>
</tr>
<tr>
<td><pre
style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">$FAILED_TESTS</pre>
<br /></td>
<td>
<pre style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">$FAILED_TESTS</pre>
<br />
</td>
</tr>
<tr>
<td><b><font color="#0B610B">构建日志 (最后 100行):</font></b>
<hr size="2" width="100%" align="center" /></td>
<td>
<b><font color="#0B610B">构建日志 (最后 100行):</font></b>
<hr size="2" width="100%" align="center" />
</td>
</tr>
<tr>
<td>Test Logs (if test has ran):
<a href="${PROJECT_URL}ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip">${PROJECT_URL}/ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip</a>
<td>
Test Logs (if test has ran):
<a href="${PROJECT_URL}ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip"
>${PROJECT_URL}/ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip</a
>
<br />
<br />
</td>
</tr>
<tr>
<td><textarea cols="80" rows="30" readonly="readonly"
style="font-family: Courier New">${BUILD_LOG}</textarea>
</td>
<td><textarea cols="80" rows="30" readonly="readonly" style="font-family: Courier New">${BUILD_LOG}</textarea></td>
</tr>
</table>
</body>

View File

@@ -1,26 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta charset="UTF-8" />
<title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>
</head>
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"
offset="0">
<table width="95%" cellpadding="0" cellspacing="0"
style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0">
<table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
<tr>
<td>(本邮件是程序自动下发的,请勿回复!)</td>
</tr>
<tr>
<td><h2>
<td>
<h2>
<font color="#228B22">构建结果 - ${BUILD_STATUS}</font>
</h2></td>
</h2>
</td>
</tr>
<tr>
<td><br />
<td>
<br />
<b><font color="#0B610B">构建信息</font></b>
<hr size="2" width="100%" align="center" /></td>
<hr size="2" width="100%" align="center" />
</td>
</tr>
<tr>
<td>
@@ -36,41 +38,51 @@
</td>
</tr>
<tr>
<td><b><font color="#0B610B">Changes Since Last
Successful Build:</font></b>
<hr size="2" width="100%" align="center" /></td>
<td>
<b><font color="#0B610B">Changes Since Last Successful Build:</font></b>
<hr size="2" width="100%" align="center" />
</td>
</tr>
<tr>
<td>
<ul>
<li>历史变更记录 : <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
</ul> ${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for Build #%n:<br />%c<br />",showPaths=true,changesFormat="<pre>[%a]<br />%m</pre>",pathFormat="&nbsp;&nbsp;&nbsp;&nbsp;%p"}
</ul>
${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for Build #%n:<br />%c<br />",showPaths=true,changesFormat="
<pre>[%a]<br />%m</pre>
",pathFormat="&nbsp;&nbsp;&nbsp;&nbsp;%p"}
</td>
</tr>
<tr>
<td><b>Failed Test Results</b>
<hr size="2" width="100%" align="center" /></td>
<td>
<b>Failed Test Results</b>
<hr size="2" width="100%" align="center" />
</td>
</tr>
<tr>
<td><pre
style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">$FAILED_TESTS</pre>
<br /></td>
<td>
<pre style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">$FAILED_TESTS</pre>
<br />
</td>
</tr>
<tr>
<td><b><font color="#0B610B">构建日志 (最后 100行):</font></b>
<hr size="2" width="100%" align="center" /></td>
<td>
<b><font color="#0B610B">构建日志 (最后 100行):</font></b>
<hr size="2" width="100%" align="center" />
</td>
</tr>
<tr>
<td>Test Logs (if test has ran):
<a href="${PROJECT_URL}ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip">${PROJECT_URL}/ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip</a>
<td>
Test Logs (if test has ran):
<a href="${PROJECT_URL}ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip"
>${PROJECT_URL}/ws/TestResult/archive_logs/Log-Build-${BUILD_NUMBER}.zip</a
>
<br />
<br />
</td>
</tr>
<tr>
<td><textarea cols="80" rows="30" readonly="readonly"
style="font-family: Courier New">${BUILD_LOG}</textarea>
</td>
<td><textarea cols="80" rows="30" readonly="readonly" style="font-family: Courier New">${BUILD_LOG}</textarea></td>
</tr>
</table>
</body>

View File

@@ -154,7 +154,18 @@ watch(templateFiles, (files) => {
}, { deep: true })
const rules: FormRules = {
acceptDate: [{ required: true, message: '请选择验收日期', trigger: 'change' }],
templateFileIds: [
{
validator: (_rule: any, value: any, callback: (e?: Error) => void) => {
if (!value || (Array.isArray(value) && value.length === 0) || (typeof value === 'string' && !value.trim())) {
callback(new Error('请上传履约验收文件'))
return
}
callback()
},
trigger: 'change'
},
],
}
const validate = () => formRef.value?.validate()

View File

@@ -58,8 +58,30 @@
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
<el-form-item label="供应商联系人及电话" prop="supplierContact">
<el-input v-model="form.supplierContact" placeholder="请输入" clearable />
<el-form-item label="资产管理员" prop="assetAdminId">
<el-select
v-model="form.assetAdminId"
placeholder="请输入姓名或工号搜索"
filterable
remote
clearable
reserve-keyword
:remote-method="searchAssetAdmin"
:loading="assetAdminLoading"
style="width: 100%"
@change="onAssetAdminChange"
>
<el-option
v-for="item in assetAdminOptions"
:key="item.teacherNo"
:label="(item.commonDeptName ? item.commonDeptName + ' - ' : '') + (item.realName || item.name) + ' (' + item.teacherNo + ')'"
:value="item.teacherNo"
>
<span>{{ item.commonDeptName ? item.commonDeptName + ' - ' : '' }}{{ item.realName || item.name }}</span>
<span style="color: #999; font-size: 12px; margin-left: 8px;">{{ item.teacherNo }}</span>
</el-option>
</el-select>
<div class="field-note">如入固定资产必填</div>
</el-form-item>
</el-col>
<el-col :span="8" class="mb20">
@@ -167,7 +189,6 @@ const form = reactive({
projectName: '',
deptName: '',
supplierName: '',
supplierContact: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
@@ -336,4 +357,9 @@ defineExpose({ validate, form })
.mb20 {
margin-bottom: 20px;
}
.field-note {
font-size: 12px;
color: #999;
margin-top: 4px;
}
</style>

View File

@@ -192,7 +192,6 @@ const loadData = async () => {
isInstallment: config.common.isInstallment || '0',
totalPhases: config.common.totalPhases || 1,
supplierName: config.common.supplierName || '',
supplierContact: config.common.supplierContact || '',
transactionAmount: config.common.transactionAmount || null,
})
}
@@ -278,7 +277,6 @@ const saveCommonConfig = async () => {
isInstallment: form.isInstallment ?? '0',
totalPhases: isInstallment ? (Number(form.totalPhases) || 1) : 1,
supplierName: String(form.supplierName ?? ''),
supplierContact: String(form.supplierContact ?? ''),
purchaserId: String(form.purchaserId ?? ''),
purchaserName: String(form.purchaserName ?? ''),
assetAdminId: String(form.assetAdminId ?? ''),
@@ -307,10 +305,7 @@ const saveCurrentBatch = async () => {
const formData = batchFormRef?.getFormData?.() || batchFormRef?.form
if (!formData) return
if (!formData.acceptDate) {
useMessage().error('请选择验收日期')
return
}
// acceptDate is now optional - removed the validation check
// templateFileIds: 提取ID数组
let fileIds: string[] = []
@@ -357,7 +352,6 @@ const DEFAULT_COMMON_FORM = {
isInstallment: '0',
totalPhases: 1,
supplierName: '',
supplierContact: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',

View File

@@ -205,7 +205,7 @@
<upload-file
v-model="dataForm.businessNegotiationTable"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.businessNegotiationTable }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('businessNegotiationTable')"
@@ -226,7 +226,7 @@
<upload-file
v-model="dataForm.marketPurchaseMinutes"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.marketPurchaseMinutes }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('marketPurchaseMinutes')"
@@ -247,7 +247,7 @@
<upload-file
v-model="dataForm.onlineMallMaterials"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.onlineMallMaterials }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('onlineMallMaterials')"
@@ -259,7 +259,7 @@
<upload-file
v-model="dataForm.inquiryTemplate"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.inquiryTemplate }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('inquiryTemplate')"
@@ -278,13 +278,13 @@
<el-col
:span="8"
class="mb12"
v-if="isDeptPurchase && dataForm.budget != null && dataForm.budget >= BUDGET_DEPT_SELF_MEETING_MINUTES"
v-if="dataForm.budget != null && dataForm.budget >= BUDGET_DEPT_SELF_MEETING_MINUTES"
>
<el-form-item label="部门自行采购会议纪要" prop="deptSelfMeetingMinutes" required>
<upload-file
v-model="dataForm.deptSelfMeetingMinutes"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.deptSelfMeetingMinutes }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('deptSelfMeetingMinutes')"
@@ -320,7 +320,7 @@
<upload-file
v-model="dataForm.serviceDirectSelect"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.serviceDirectSelect }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('serviceDirectSelect')"
@@ -351,7 +351,7 @@
<upload-file
v-model="dataForm.serviceInviteSelect"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.serviceInviteSelect }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('serviceInviteSelect')"
@@ -376,7 +376,7 @@
<upload-file
v-model="dataForm.purchaseRequirementTemplate"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx']"
:data="{ fileType: FILE_TYPE_MAP.purchaseRequirementTemplate }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('purchaseRequirementTemplate')"
@@ -405,7 +405,7 @@
<upload-file
v-model="dataForm.serviceInviteSelect"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.serviceInviteSelect }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('serviceInviteSelect')"
@@ -426,7 +426,7 @@
<upload-file
v-model="dataForm.servicePublicSelectAuto"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.servicePublicSelectAuto }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('servicePublicSelectAuto')"
@@ -556,7 +556,7 @@
<upload-file
v-model="dataForm.feasibilityReport"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.feasibilityReport }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('feasibilityReport')"
@@ -577,41 +577,28 @@
class="mb12"
v-if="
dataForm.budget &&
dataForm.budget >= BUDGET_FEASIBILITY_THRESHOLD &&
!isUrgentSpecial &&
!isSpecialType('2') &&
!isSpecialType('3')
(dataForm.budget >= BUDGET_FEASIBILITY_THRESHOLD || !isSpecialType('0'))
"
>
<el-form-item label="校党委会议纪要" prop="meetingMinutes" required>
<upload-file
v-model="dataForm.meetingMinutes"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.meetingMinutes }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('meetingMinutes')"
/>
</el-form-item>
</el-col>
<el-col :span="8" class="mb12" v-if="isUrgentSpecial">
<el-form-item label="校党委会议纪要(紧急)" prop="meetingMinutesUrgent" required>
<upload-file
v-model="dataForm.meetingMinutesUrgent"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:data="{ fileType: FILE_TYPE_MAP.meetingMinutesUrgent }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('meetingMinutesUrgent')"
/>
</el-form-item>
</el-col>
<el-col :span="8" class="mb12" v-if="isSpecialType('2')">
<el-form-item label="单一来源论专家证附件" prop="singleSourceProof" required>
<upload-file
v-model="dataForm.singleSourceProof"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.singleSourceProof }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('singleSourceProof')"
@@ -632,7 +619,7 @@
<upload-file
v-model="dataForm.meetingMinutesSingle"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.meetingMinutesSingle }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('meetingMinutesSingle')"
@@ -644,7 +631,7 @@
<upload-file
v-model="dataForm.importApplication"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.importApplication }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('importApplication')"
@@ -665,7 +652,7 @@
<upload-file
v-model="dataForm.meetingMinutesImport"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.meetingMinutesImport }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('meetingMinutesImport')"
@@ -693,7 +680,7 @@
<upload-file
v-model="dataForm.serviceInviteSelectSchool"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.serviceInviteSelectSchool }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('serviceInviteSelectSchool')"
@@ -712,7 +699,7 @@
<upload-file
v-model="dataForm.servicePublicSelectSchoolAuto"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.servicePublicSelectSchoolAuto }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('servicePublicSelectSchoolAuto')"
@@ -732,7 +719,7 @@
<upload-file
v-model="dataForm.servicePublicSelectSchool"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.servicePublicSelectSchool }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('servicePublicSelectSchool')"
@@ -751,7 +738,7 @@
<upload-file
v-model="dataForm.purchaseRequirement"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.purchaseRequirement }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('purchaseRequirement')"
@@ -773,7 +760,7 @@
<upload-file
v-model="dataForm.governmentPurchaseIntent"
:limit="1"
:file-type="['doc', 'docx', 'pdf']"
:file-type="['doc', 'docx', 'pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']"
:data="{ fileType: FILE_TYPE_MAP.governmentPurchaseIntent }"
upload-file-url="/purchase/purchasingfiles/upload"
:disabled="flowFieldDisabled('governmentPurchaseIntent')"
@@ -1285,9 +1272,9 @@ const schoolUnifiedPurchaseFormDefault = computed(() => {
// 申请阶段:始终可选(根据默认值自动选中后,允许用户自行修改)
// 流程嵌入:采购中心节点可编辑,其他节点只读
const schoolUnifiedPurchaseFormDisabled = computed(() => {
if (!isFlowEmbed.value) {
return false;
}
// if (!isFlowEmbed.value) {
// return false;
// }
// 流程嵌入且为采购中心:放开编辑
if (isPurchaseCenter.value) {
return false;
@@ -1610,6 +1597,7 @@ const dataRules = reactive({
],
purchaseChannel: [
{
required: true,
validator: (_rule: any, value: string, callback: (e?: Error) => void) => {
if (!isDeptPurchase.value) {
callback();