mirror of
http://183.237.82.114:10086/kilo/leaser_front.git
synced 2026-10-23 17:35:04 +00:00
feat: 合同文档模板设置
This commit is contained in:
parent
fc07ed06d5
commit
8e43e1835f
38 changed files with 4560 additions and 1410 deletions
|
|
@ -1,3 +1,7 @@
|
|||
/*.json
|
||||
/*.js
|
||||
dist
|
||||
dist
|
||||
*.config.js
|
||||
*.config.ts
|
||||
.eslintrc.js
|
||||
node_modules/
|
||||
|
|
|
|||
146
.eslintrc.js
146
.eslintrc.js
|
|
@ -1,77 +1,129 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
// Parser that checks the content of the <script> tag
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
env: {
|
||||
'browser': true,
|
||||
'node': true,
|
||||
'es2021': true,
|
||||
'vue/setup-compiler-macros': true,
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
// 1. 主解析器始终是 'vue-eslint-parser'
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
// 2. 在顶层 parserOptions 中指定 TS 解析器和 JSX 支持
|
||||
parser: '@typescript-eslint/parser',
|
||||
ecmaVersion: 'latest', // 使用 'latest' 更稳妥
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true, // 关键:开启 JSX 支持
|
||||
},
|
||||
extraFileExtensions: ['.vue'],
|
||||
project: path.resolve(__dirname, './tsconfig.json'),
|
||||
},
|
||||
extends: [
|
||||
// Airbnb JavaScript Style Guide https://github.com/airbnb/javascript
|
||||
'airbnb-base',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: path.resolve(__dirname, './tsconfig.json'),
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: ['vue', '@typescript-eslint', 'import', 'prettier'],
|
||||
rules: {
|
||||
// 你的规则保持不变
|
||||
'no-param-reassign': 0,
|
||||
'no-plusplus': 0,
|
||||
'prettier/prettier': 1,
|
||||
// Vue: Recommended rules to be closed or modify
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 2 : 1,
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'no-unused-expressions': 'off',
|
||||
|
||||
'vue/multi-word-component-names': 0,
|
||||
'vue/require-default-prop': 0,
|
||||
'vue/singleline-html-element-content-newline': 0,
|
||||
'vue/max-attributes-per-line': 0,
|
||||
// Vue: Add extra rules
|
||||
'vue/custom-event-name-casing': [0, 'camelCase'],
|
||||
'camelcase': ['warn', { properties: 'never' }],
|
||||
'vue/no-v-text': 1,
|
||||
'vue/padding-line-between-blocks': 1,
|
||||
'vue/require-direct-export': 1,
|
||||
'vue/multi-word-component-names': 0,
|
||||
// Allow @ts-ignore comment
|
||||
'@typescript-eslint/ban-ts-comment': 0,
|
||||
'@typescript-eslint/no-unused-vars': 1,
|
||||
'@typescript-eslint/no-empty-function': 1,
|
||||
'@typescript-eslint/no-explicit-any': 0, // 改为警告
|
||||
'no-unused-expressions': 'off',
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 2 : 1, // 生产环境禁用console
|
||||
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
|
||||
'import/no-unresolved': [2, { ignore: ['^@/'] }],
|
||||
'import/extensions': [
|
||||
2,
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
vue: 'off',
|
||||
},
|
||||
],
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
'no-param-reassign': 0,
|
||||
'prefer-regex-literals': 0,
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
// 禁用 prefer-destructuring 规则,因为它对对象解构和数组解构都产生错误警告
|
||||
'prefer-destructuring': 0,
|
||||
'no-return-assign': ['error', 'except-parens'],
|
||||
},
|
||||
|
||||
overrides: [
|
||||
// 3. 为 .vue 文件提供更具体的解析器选项(如果需要)
|
||||
{
|
||||
files: ['*.vue'],
|
||||
rules: {
|
||||
// 在这里可以覆盖一些只针对 .vue 文件的规则
|
||||
// 例如,vue-eslint-parser 已经处理了未使用的变量,所以可以关闭 TS 的检查
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
// 4. 为 .tsx 文件单独配置(推荐)
|
||||
{
|
||||
files: ['*.tsx'],
|
||||
parser: '@typescript-eslint/parser', // 直接使用 TS 解析器
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
project: path.resolve(__dirname, './tsconfig.json'),
|
||||
},
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
},
|
||||
// d.ts 文件配置保持不变
|
||||
{
|
||||
files: ['**/*.d.ts'],
|
||||
parserOptions: { project: undefined },
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.config.ts', '*.config.js'], // 匹配所有 .config.ts 和 .config.js 文件
|
||||
parserOptions: {
|
||||
project: undefined, // 关键:告诉 ESLint 这个文件不需要关联 TS 项目配置
|
||||
},
|
||||
rules: {
|
||||
// 在这里可以关闭一些对配置文件不适用的规则
|
||||
'import/no-named-as-default': 'off', // 例如,这个规则在配置文件中很常见
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
project: path.resolve(__dirname, './tsconfig.json'),
|
||||
},
|
||||
node: {
|
||||
extensions: ['.js', '.ts', '.tsx', '.vue'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
ignorePatterns: ['.eslintrc.js', 'node_modules', 'dist', '*.config.js'],
|
||||
};
|
||||
|
|
|
|||
29
package.json
29
package.json
|
|
@ -24,6 +24,9 @@
|
|||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.d.ts": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{less,css}": [
|
||||
"stylelint --fix",
|
||||
"prettier --write"
|
||||
|
|
@ -31,6 +34,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.57.0",
|
||||
"@onlyoffice/document-editor-vue": "^1.6.1",
|
||||
"@vueuse/core": "^9.3.0",
|
||||
"arco-design-pro-vue": "^2.7.3",
|
||||
"axios": "^0.24.0",
|
||||
|
|
@ -39,7 +43,7 @@
|
|||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.23",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.3.0",
|
||||
"query-string": "^8.0.3",
|
||||
"sortablejs": "^1.15.0",
|
||||
|
|
@ -53,38 +57,42 @@
|
|||
"@arco-plugins/vite-vue": "^1.4.5",
|
||||
"@commitlint/cli": "^17.1.2",
|
||||
"@commitlint/config-conventional": "^17.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/mockjs": "^1.0.7",
|
||||
"@types/node": "^24.0.3",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"@vitejs/plugin-vue": "^3.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||
"@vue/babel-plugin-jsx": "^1.1.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"consola": "^2.15.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.6.0",
|
||||
"eslint-config-prettier": "^8.10.2",
|
||||
"eslint-import-resolver-typescript": "^3.10.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^4.2.5",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"globals": "^16.4.0",
|
||||
"husky": "^8.0.1",
|
||||
"less": "^4.1.3",
|
||||
"lint-staged": "^13.0.3",
|
||||
"mockjs": "^1.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-html": "^1.8.0",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier": "^2.8.8",
|
||||
"rollup": "^3.9.1",
|
||||
"rollup-plugin-visualizer": "^5.8.2",
|
||||
"stylelint": "^16.21.0",
|
||||
"stylelint-config-recommended-vue": "^1.5.0",
|
||||
"stylelint-config-standard": "^36.0.1",
|
||||
"stylelint-order": "^6.0.4",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^4.8.4",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
|
|
@ -93,6 +101,7 @@
|
|||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-imagemin": "^0.6.1",
|
||||
"vite-svg-loader": "^3.6.0",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^1.0.14"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
|||
2482
pnpm-lock.yaml
generated
2482
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
6
postcss.config.cjs
Normal file
6
postcss.config.cjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ import {
|
|||
CreateTemplateSettingParam,
|
||||
ContractWordFormParam,
|
||||
ContractWordSettingListRes,
|
||||
SettingContractFileInfoRes,
|
||||
} from '@/types/setting';
|
||||
import { PaginationParams, TenantDictionaryType } from '@/types/comm';
|
||||
import { HttpResponse } from './interceptor';
|
||||
|
|
@ -346,7 +347,7 @@ export function getCodeSettingList(
|
|||
return axios.post(`contractSetting/getCodeSettingList`, params);
|
||||
}
|
||||
// 合同设置-合同条款设置-全部启用列表
|
||||
export function getAllTermsSettingList(): Promise<SettingContractNumbeRes> {
|
||||
export function getAllTermsSettingList(): Promise<any> {
|
||||
return axios.get(`contractSetting/getAllTermsSettingList`);
|
||||
}
|
||||
// 合同设置-合同条款设置-新增
|
||||
|
|
@ -416,6 +417,10 @@ export function getUseFeeList(
|
|||
): Promise<FeeManagementListRes> {
|
||||
return axios.post(`financeSetting/getUseFeeList`, params);
|
||||
}
|
||||
// 合同设置-流水账户管理--获取下拉列表
|
||||
export function getUseTurnoverAccountList(params: any): Promise<any> {
|
||||
return axios.get(`financeSetting/getUseTurnoverAccountList`, params);
|
||||
}
|
||||
|
||||
// 合同设置-合同模板管理-全部启用列表
|
||||
export function getAllTemplateSettingList(): Promise<SettingContractNumbeRes> {
|
||||
|
|
@ -428,19 +433,15 @@ export function createTemplateSetting(
|
|||
return axios.post(`contractSetting/createTemplateSetting`, params);
|
||||
}
|
||||
// 合同设置-合同模板管理-编辑
|
||||
export function updateTemplateSetting(
|
||||
params: CreateTemplateSettingParam
|
||||
): Promise<HttpResponse> {
|
||||
return axios.post(`contractSetting/updateTemplateSetting`, params);
|
||||
export function updateTemplateSetting(params: any): Promise<HttpResponse> {
|
||||
return axios.post(`contractSetting/saveTemplateSetting`, params);
|
||||
}
|
||||
// 合同设置-合同模板管理-删除
|
||||
export function delTemplateSetting(id: number): Promise<HttpResponse> {
|
||||
return axios.get(`contractSetting/delTemplateSetting?id=${id}`);
|
||||
}
|
||||
// 合同设置-合同模板管理-单个详情
|
||||
export function getTemplateSettingFromId(
|
||||
id: number
|
||||
): Promise<SettingContractNumbeRes> {
|
||||
export function getTemplateSettingFromId(id: number): Promise<any> {
|
||||
return axios.get(`contractSetting/getTemplateSetting?id=${id}`);
|
||||
}
|
||||
// 合同设置-合同模板管理-启用/禁用
|
||||
|
|
@ -479,7 +480,7 @@ export function delWordSetting(id: number): Promise<HttpResponse> {
|
|||
// 合同设置-合同文档模板管理-单个详情
|
||||
export function getWordSettingFromId(
|
||||
id: number
|
||||
): Promise<SettingContractNumbeRes> {
|
||||
): Promise<SettingContractFileInfoRes> {
|
||||
return axios.get(`contractSetting/getWordSetting?id=${id}`);
|
||||
}
|
||||
// 合同设置-合同文档模板管理-启用/禁用
|
||||
|
|
@ -494,3 +495,47 @@ export function getWordSettingList(
|
|||
): Promise<ContractWordSettingListRes> {
|
||||
return axios.post(`contractSetting/getWordSettingList`, params);
|
||||
}
|
||||
// 合同标签列表(已勾选)
|
||||
export function getContractTagsList(): Promise<any> {
|
||||
return axios.get(`dictionary/getContractTagsList`);
|
||||
}
|
||||
// 数据字典-合同管理-合同标签列表(全部)
|
||||
export function getAllContractTagsList(): Promise<any> {
|
||||
return axios.get(`dictionary/getAllContractTagsList`);
|
||||
}
|
||||
// 数据字典-合同管理-创建合同标签
|
||||
export function createContractTags(
|
||||
params: DictionaryForm
|
||||
): Promise<HttpResponse> {
|
||||
return axios.post(`dictionary/createContractTags`, params);
|
||||
}
|
||||
// 数据字典-合同管理-更新合同标签
|
||||
export function updateContractTags(
|
||||
params: DictionaryForm
|
||||
): Promise<HttpResponse> {
|
||||
return axios.post(`dictionary/updateContractTags`, params);
|
||||
}
|
||||
// 数据字典-合同管理-删除合同标签
|
||||
export function delContractTags(
|
||||
id: number
|
||||
): Promise<SettingProjectProvinceRes> {
|
||||
return axios.get(`dictionary/delContractTags?id=${id}`);
|
||||
}
|
||||
// 数据字典-合同管理-禁用/启用 合同标签(单个)
|
||||
export function checkContractTags(
|
||||
params: CheckProjectParam
|
||||
): Promise<HttpResponse> {
|
||||
return axios.post(`dictionary/checkContractTags`, params);
|
||||
}
|
||||
// 数据字典-合同管理-禁用/启用 合同标签(全选)
|
||||
export function checkAllContractTags(
|
||||
params: CheckProjectParam
|
||||
): Promise<HttpResponse> {
|
||||
return axios.post(`dictionary/checkAllContractTags`, params);
|
||||
}
|
||||
// 数据字典-合同管理-单个合同标签信息
|
||||
export function getContractTags(
|
||||
id: number
|
||||
): Promise<SettingProjectProvinceRes> {
|
||||
return axios.get(`dictionary/getContractTags?id=${id}`);
|
||||
}
|
||||
|
|
|
|||
3
src/assets/style/index.css
Normal file
3
src/assets/style/index.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
431
src/components/common/OnlyOfficeEditor.vue
Normal file
431
src/components/common/OnlyOfficeEditor.vue
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
<template>
|
||||
<div class="onlyoffice-editor">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<a-spin size="large" />
|
||||
<p class="mt-4">正在加载文档编辑器...</p>
|
||||
</div>
|
||||
<div v-else-if="error" class="error-container">
|
||||
<a-alert
|
||||
type="error"
|
||||
:message="error"
|
||||
show-icon
|
||||
closable
|
||||
@close="error = ''"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
id="onlyoffice-editor"
|
||||
ref="editorContainer"
|
||||
class="editor-container"
|
||||
>
|
||||
<!-- 临时测试显示 -->
|
||||
<div class="test-editor-placeholder">
|
||||
<div class="editor-header">
|
||||
<h3>{{ props.documentTitle }}</h3>
|
||||
<div class="editor-actions">
|
||||
<a-button type="primary" size="small">保存</a-button>
|
||||
<a-button size="small">关闭</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-content">
|
||||
<div class="document-info">
|
||||
<p><strong>文档类型:</strong> {{ props.documentType }}</p>
|
||||
<p><strong>文档地址:</strong> {{ props.documentUrl }}</p>
|
||||
<p><strong>编辑模式:</strong> {{ props.mode }}</p>
|
||||
<p><strong>可编辑:</strong> {{ props.editable ? '是' : '否' }}</p>
|
||||
</div>
|
||||
<div class="editor-mock">
|
||||
<div class="toolbar">
|
||||
<a-button size="small">文件</a-button>
|
||||
<a-button size="small">编辑</a-button>
|
||||
<a-button size="small">视图</a-button>
|
||||
<a-button size="small">插入</a-button>
|
||||
<a-button size="small">格式</a-button>
|
||||
</div>
|
||||
<div class="document-area">
|
||||
<p>这里是文档编辑区域</p>
|
||||
<p>OnlyOffice 编辑器将在这里显示</p>
|
||||
<p>当前文档: {{ props.documentTitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
interface Props {
|
||||
documentUrl: string;
|
||||
documentTitle?: string;
|
||||
documentType?: 'word' | 'excel' | 'powerpoint';
|
||||
height?: string;
|
||||
width?: string;
|
||||
editable?: boolean;
|
||||
mode?: 'edit' | 'view';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
documentTitle: '文档',
|
||||
documentType: 'word',
|
||||
height: '600px',
|
||||
width: '100%',
|
||||
editable: true,
|
||||
mode: 'edit',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready: [];
|
||||
error: [error: string];
|
||||
save: [data: any];
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const editorContainer = ref<HTMLElement>();
|
||||
const loading = ref(true);
|
||||
const error = ref('');
|
||||
let docEditor: any = null;
|
||||
|
||||
// 动态加载 OnlyOffice API
|
||||
const loadOnlyOfficeAPI = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof (window as any).DocsAPI !== 'undefined') {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
// 使用多个备选 API 地址
|
||||
const apiUrls = [
|
||||
'https://documentserver/web-apps/apps/api/documents/api.js', // 本地 Document Server
|
||||
'https://api.onlyoffice.com/editors/latest/web-apps/apps/api/documents/api.js', // 官方 API
|
||||
'https://documentserver.onlyoffice.com/web-apps/apps/api/documents/api.js', // 备用地址
|
||||
];
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
const tryLoadScript = () => {
|
||||
if (currentIndex >= apiUrls.length) {
|
||||
reject(new Error('所有 OnlyOffice API 地址都无法访问'));
|
||||
return;
|
||||
}
|
||||
|
||||
script.src = apiUrls[currentIndex];
|
||||
script.onload = () => {
|
||||
console.log(`OnlyOffice API 加载成功: ${apiUrls[currentIndex]}`);
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => {
|
||||
console.warn(`OnlyOffice API 加载失败: ${apiUrls[currentIndex]}`);
|
||||
currentIndex++;
|
||||
tryLoadScript();
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
};
|
||||
|
||||
tryLoadScript();
|
||||
});
|
||||
};
|
||||
|
||||
// 获取文档类型对应的文件扩展名
|
||||
const getFileExtension = (type: string) => {
|
||||
const extensions = {
|
||||
word: '.docx',
|
||||
excel: '.xlsx',
|
||||
powerpoint: '.pptx',
|
||||
};
|
||||
return extensions[type as keyof typeof extensions] || '.docx';
|
||||
};
|
||||
|
||||
// 生成文档唯一标识
|
||||
function generateDocumentKey() {
|
||||
return 'doc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
// 编辑器事件处理
|
||||
function onAppReady() {
|
||||
console.log('OnlyOffice 编辑器已准备就绪');
|
||||
loading.value = false;
|
||||
emit('ready');
|
||||
}
|
||||
|
||||
function onDocumentStateChange(event: any) {
|
||||
console.log('文档状态变化:', event);
|
||||
if (event.data === 1) {
|
||||
// 文档正在编辑
|
||||
Message.info('文档正在编辑中...');
|
||||
} else if (event.data === 2) {
|
||||
// 文档已保存
|
||||
Message.success('文档已保存');
|
||||
emit('save', event);
|
||||
}
|
||||
}
|
||||
|
||||
function onRequestSaveAs(event: any) {
|
||||
console.log('请求另存为:', event);
|
||||
// 处理另存为请求
|
||||
}
|
||||
|
||||
function onError(event: any) {
|
||||
console.error('OnlyOffice 错误:', event);
|
||||
error.value = event.data || '编辑器发生错误';
|
||||
emit('error', error.value);
|
||||
}
|
||||
|
||||
function onWarning(event: any) {
|
||||
console.warn('OnlyOffice 警告:', event);
|
||||
}
|
||||
|
||||
function onRequestClose() {
|
||||
console.log('请求关闭编辑器');
|
||||
emit('close');
|
||||
}
|
||||
|
||||
// 销毁编辑器
|
||||
function destroyEditor() {
|
||||
if (docEditor) {
|
||||
try {
|
||||
docEditor.destroyEditor();
|
||||
} catch (err) {
|
||||
console.error('销毁编辑器失败:', err);
|
||||
}
|
||||
docEditor = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 OnlyOffice 编辑器
|
||||
async function initEditor() {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
if (!props.documentUrl) {
|
||||
throw new Error('文档 URL 不能为空');
|
||||
}
|
||||
|
||||
// 检查是否已经加载了 OnlyOffice API
|
||||
if (typeof (window as any).DocsAPI === 'undefined') {
|
||||
// 动态加载 OnlyOffice API
|
||||
try {
|
||||
await loadOnlyOfficeAPI();
|
||||
} catch (apiError) {
|
||||
console.warn('OnlyOffice API 加载失败,显示测试界面:', apiError);
|
||||
// API 加载失败时,直接显示测试界面
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑器配置
|
||||
const config = {
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
type: props.mode === 'edit' ? 'desktop' : 'embedded',
|
||||
documentType: props.documentType,
|
||||
document: {
|
||||
fileType: getFileExtension(props.documentType),
|
||||
key: generateDocumentKey(),
|
||||
title: props.documentTitle,
|
||||
url: props.documentUrl,
|
||||
permissions: {
|
||||
edit: props.editable && props.mode === 'edit',
|
||||
download: true,
|
||||
print: true,
|
||||
review: props.mode === 'edit',
|
||||
},
|
||||
},
|
||||
editorConfig: {
|
||||
mode: props.mode === 'edit' ? 'edit' : 'view',
|
||||
lang: 'zh-CN',
|
||||
location: 'zh-CN',
|
||||
customization: {
|
||||
autosave: true,
|
||||
forcesave: true,
|
||||
chat: false,
|
||||
comments: props.mode === 'edit',
|
||||
help: true,
|
||||
hideRightMenu: false,
|
||||
hideRulers: false,
|
||||
integrationMode: 'embedded',
|
||||
zoom: 100,
|
||||
uiTheme: 'theme-classic-light',
|
||||
},
|
||||
callbackUrl: '', // 如果需要回调,可以设置服务器回调地址
|
||||
user: {
|
||||
id: 'user-1',
|
||||
name: '用户',
|
||||
},
|
||||
},
|
||||
events: {
|
||||
onAppReady: onAppReady,
|
||||
onDocumentStateChange: onDocumentStateChange,
|
||||
onRequestSaveAs: onRequestSaveAs,
|
||||
onError: onError,
|
||||
onWarning: onWarning,
|
||||
onRequestClose: onRequestClose,
|
||||
},
|
||||
};
|
||||
|
||||
// 创建编辑器实例
|
||||
docEditor = new (window as any).DocsAPI.DocEditor(
|
||||
'onlyoffice-editor',
|
||||
config
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('OnlyOffice 初始化失败:', err);
|
||||
error.value = err instanceof Error ? err.message : '编辑器初始化失败';
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听文档 URL 变化
|
||||
watch(
|
||||
() => props.documentUrl,
|
||||
(newUrl) => {
|
||||
if (newUrl && editorContainer.value) {
|
||||
destroyEditor();
|
||||
nextTick(() => {
|
||||
initEditor();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
if (props.documentUrl) {
|
||||
initEditor();
|
||||
} else {
|
||||
loading.value = false;
|
||||
error.value = '请提供文档 URL';
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
destroyEditor();
|
||||
});
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
destroyEditor,
|
||||
getEditor: () => docEditor,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.onlyoffice-editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
#onlyoffice-editor {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 测试编辑器样式 */
|
||||
.test-editor-placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.editor-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.document-info {
|
||||
padding: 16px;
|
||||
background: #f7f8fa;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.document-info p {
|
||||
margin: 4px 0;
|
||||
color: #4e5969;
|
||||
}
|
||||
|
||||
.editor-mock {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #f7f8fa;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
}
|
||||
|
||||
.document-area {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.document-area p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -13,6 +13,7 @@ import Chart from './chart/index.vue';
|
|||
import Breadcrumb from './breadcrumb/index.vue';
|
||||
import DataTable from './common/DataTable.vue';
|
||||
import ImageUpload from './common/ImageUpload.vue';
|
||||
import OnlyOfficeEditor from './common/OnlyOfficeEditor.vue';
|
||||
|
||||
// Manually introduce ECharts modules to reduce packing size
|
||||
|
||||
|
|
@ -35,5 +36,6 @@ export default {
|
|||
Vue.component('Breadcrumb', Breadcrumb);
|
||||
Vue.component('DataTable', DataTable);
|
||||
Vue.component('ImageUpload', ImageUpload);
|
||||
Vue.component('OnlyOfficeEditor', OnlyOfficeEditor);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,158 +1,23 @@
|
|||
<script lang="tsx">
|
||||
import { defineComponent, ref, h, compile, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter, RouteRecordRaw } from 'vue-router';
|
||||
import type { RouteMeta } from 'vue-router';
|
||||
import { useAppStore } from '@/store';
|
||||
import { listenerRouteChange } from '@/utils/route-listener';
|
||||
import { openWindow, regexUrl } from '@/utils';
|
||||
import useMenuTree from './use-menu-tree';
|
||||
<!-- src/components/menu/index.vue -->
|
||||
<template>
|
||||
<!-- 直接使用 TSX 组件,与普通 Vue 组件无区别 -->
|
||||
<MenuContent />
|
||||
</template>
|
||||
|
||||
export default defineComponent({
|
||||
emit: ['collapse'],
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { menuTree } = useMenuTree();
|
||||
const collapsed = computed({
|
||||
get() {
|
||||
if (appStore.device === 'desktop') return appStore.menuCollapse;
|
||||
return false;
|
||||
},
|
||||
set(value: boolean) {
|
||||
appStore.updateSettings({ menuCollapse: value });
|
||||
},
|
||||
});
|
||||
|
||||
const topMenu = computed(() => appStore.topMenu);
|
||||
const openKeys = ref<string[]>([]);
|
||||
const selectedKey = ref<string[]>([]);
|
||||
|
||||
const goto = (item: RouteRecordRaw) => {
|
||||
// Open external link
|
||||
if (regexUrl.test(item.path)) {
|
||||
openWindow(item.path);
|
||||
selectedKey.value = [item.name as string];
|
||||
return;
|
||||
}
|
||||
// Eliminate external link side effects
|
||||
const { hideInMenu, activeMenu } = item.meta as RouteMeta;
|
||||
if (route.name === item.name && !hideInMenu && !activeMenu) {
|
||||
selectedKey.value = [item.name as string];
|
||||
return;
|
||||
}
|
||||
// Trigger router change
|
||||
router.push({
|
||||
name: item.name,
|
||||
});
|
||||
};
|
||||
const findMenuOpenKeys = (target: string) => {
|
||||
const result: string[] = [];
|
||||
let isFind = false;
|
||||
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
|
||||
if (item.name === target) {
|
||||
isFind = true;
|
||||
result.push(...keys);
|
||||
return;
|
||||
}
|
||||
if (item.children?.length) {
|
||||
item.children.forEach((el) => {
|
||||
backtrack(el, [...keys, el.name as string]);
|
||||
});
|
||||
}
|
||||
};
|
||||
menuTree.value.forEach((el: RouteRecordRaw) => {
|
||||
if (isFind) return; // Performance optimization
|
||||
backtrack(el, [el.name as string]);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
listenerRouteChange((newRoute) => {
|
||||
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
|
||||
if (requiresAuth && (!hideInMenu || activeMenu)) {
|
||||
const menuOpenKeys = findMenuOpenKeys(
|
||||
(activeMenu || newRoute.name) as string
|
||||
);
|
||||
|
||||
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
|
||||
openKeys.value = [...keySet];
|
||||
|
||||
// 修复选中逻辑:优先使用 activeMenu,否则使用当前路由名称
|
||||
const targetKey = activeMenu || newRoute.name;
|
||||
if (targetKey) {
|
||||
selectedKey.value = [targetKey as string];
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
const setCollapse = (val: boolean) => {
|
||||
if (appStore.device === 'desktop')
|
||||
appStore.updateSettings({ menuCollapse: val });
|
||||
};
|
||||
|
||||
const renderSubMenu = () => {
|
||||
function travel(_route: RouteRecordRaw[], nodes = []) {
|
||||
if (_route) {
|
||||
_route.forEach((element) => {
|
||||
// This is demo, modify nodes as needed
|
||||
const icon = element?.meta?.icon
|
||||
? () => h(compile(`<${element?.meta?.icon}/>`))
|
||||
: null;
|
||||
const node =
|
||||
element?.children && element?.children.length !== 0 ? (
|
||||
<a-sub-menu
|
||||
key={element?.name}
|
||||
v-slots={{
|
||||
icon,
|
||||
title: () => h(compile(t(element?.meta?.locale || ''))),
|
||||
}}
|
||||
>
|
||||
{travel(element?.children)}
|
||||
</a-sub-menu>
|
||||
) : (
|
||||
<a-menu-item
|
||||
key={element?.name}
|
||||
v-slots={{ icon }}
|
||||
onClick={() => goto(element)}
|
||||
>
|
||||
{t(element?.meta?.locale || '')}
|
||||
</a-menu-item>
|
||||
);
|
||||
nodes.push(node as never);
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
return travel(menuTree.value);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<a-menu
|
||||
mode={topMenu.value ? 'horizontal' : 'vertical'}
|
||||
v-model:collapsed={collapsed.value}
|
||||
v-model:open-keys={openKeys.value}
|
||||
show-collapse-button={appStore.device !== 'mobile'}
|
||||
auto-open={true}
|
||||
selected-keys={selectedKey.value}
|
||||
auto-open-selected={true}
|
||||
level-indent={34}
|
||||
style="height: 100%;width:100%;"
|
||||
onCollapse={setCollapse}
|
||||
>
|
||||
{renderSubMenu()}
|
||||
</a-menu>
|
||||
);
|
||||
},
|
||||
});
|
||||
<!-- 替换原 TSX 脚本,改为导入逻辑 -->
|
||||
<script setup lang="ts">
|
||||
// 导入独立的 TSX 组件
|
||||
import MenuContent from './menu';
|
||||
</script>
|
||||
|
||||
<!-- 保留原有的样式,无需修改 -->
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-menu-inner) {
|
||||
.arco-menu-inline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.arco-icon {
|
||||
&:not(.arco-icon-down) {
|
||||
font-size: 18px;
|
||||
|
|
|
|||
180
src/components/menu/menu.tsx
Normal file
180
src/components/menu/menu.tsx
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
// src/components/menu/MenuContent.tsx
|
||||
import { defineComponent, ref, h, compile, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter, RouteRecordRaw } from 'vue-router';
|
||||
import type { RouteMeta } from 'vue-router';
|
||||
import { useAppStore } from '@/store';
|
||||
import { listenerRouteChange } from '@/utils/route-listener';
|
||||
import { openWindow, regexUrl } from '@/utils';
|
||||
import useMenuTree from './use-menu-tree';
|
||||
|
||||
// 直接导出 TSX 组件(无需 .vue 包裹)
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { menuTree } = useMenuTree();
|
||||
const collapsed = computed({
|
||||
get() {
|
||||
if (appStore.device === 'desktop') return appStore.menuCollapse;
|
||||
return false;
|
||||
},
|
||||
set(value: boolean) {
|
||||
appStore.updateSettings({ menuCollapse: value });
|
||||
},
|
||||
});
|
||||
|
||||
const topMenu = computed(() => appStore.topMenu);
|
||||
const openKeys = ref<string[]>([]);
|
||||
const selectedKey = ref<string[]>([]);
|
||||
|
||||
const goto = (item: RouteRecordRaw) => {
|
||||
if (regexUrl.test(item.path)) {
|
||||
openWindow(item.path);
|
||||
selectedKey.value = [item.name as string];
|
||||
return;
|
||||
}
|
||||
if (item.redirect) {
|
||||
router.push(item.redirect);
|
||||
return;
|
||||
}
|
||||
const { hideInMenu, activeMenu } = item.meta as RouteMeta;
|
||||
if (route.name === item.name && !hideInMenu && !activeMenu) {
|
||||
selectedKey.value = [item.name as string];
|
||||
return;
|
||||
}
|
||||
router.push({ name: item.name });
|
||||
};
|
||||
|
||||
const findMenuOpenKeys = (target: string) => {
|
||||
const result: string[] = [];
|
||||
let isFind = false;
|
||||
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
|
||||
if (item.name === target) {
|
||||
isFind = true;
|
||||
result.push(...keys);
|
||||
return;
|
||||
}
|
||||
if (item.children?.length) {
|
||||
item.children.forEach((el) =>
|
||||
backtrack(el, [...keys, el.name as string])
|
||||
);
|
||||
}
|
||||
};
|
||||
menuTree.value.forEach((el: RouteRecordRaw) => {
|
||||
if (isFind) return;
|
||||
backtrack(el, [el.name as string]);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
listenerRouteChange((newRoute) => {
|
||||
const { requiresAuth, activeMenu, hideInMenu } = newRoute.meta;
|
||||
if (requiresAuth && (!hideInMenu || activeMenu)) {
|
||||
const menuOpenKeys = findMenuOpenKeys(
|
||||
(activeMenu || newRoute.name) as string
|
||||
);
|
||||
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
|
||||
openKeys.value = [...keySet];
|
||||
|
||||
let targetKey = activeMenu || newRoute.name;
|
||||
if (targetKey && !activeMenu) {
|
||||
const findParentWithHiddenChildren = (
|
||||
routes: RouteRecordRaw[],
|
||||
targetName: string
|
||||
): string | null => {
|
||||
for (const route of routes) {
|
||||
if (route.children) {
|
||||
const hasTargetChild = route.children.some(
|
||||
(child) =>
|
||||
child.name === targetName ||
|
||||
(child.children &&
|
||||
findParentWithHiddenChildren([child], targetName))
|
||||
);
|
||||
if (hasTargetChild && route.meta?.hideChildrenInMenu) {
|
||||
return route.name as string;
|
||||
}
|
||||
const parentResult = findParentWithHiddenChildren(
|
||||
route.children,
|
||||
targetName
|
||||
);
|
||||
if (parentResult) return parentResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const parentWithHiddenChildren = findParentWithHiddenChildren(
|
||||
menuTree.value,
|
||||
targetKey as string
|
||||
);
|
||||
if (parentWithHiddenChildren) targetKey = parentWithHiddenChildren;
|
||||
}
|
||||
if (targetKey) selectedKey.value = [targetKey as string];
|
||||
}
|
||||
}, true);
|
||||
|
||||
const setCollapse = (val: boolean) => {
|
||||
if (appStore.device === 'desktop')
|
||||
appStore.updateSettings({ menuCollapse: val });
|
||||
};
|
||||
|
||||
const renderSubMenu = () => {
|
||||
function travel(_route: RouteRecordRaw[], nodes = []) {
|
||||
if (_route) {
|
||||
_route.forEach((element) => {
|
||||
const icon = element?.meta?.icon
|
||||
? () => h(compile(`<${element?.meta?.icon}/>`))
|
||||
: null;
|
||||
const hideChildrenInMenu = element?.meta?.hideChildrenInMenu;
|
||||
const hasChildren =
|
||||
element?.children && element?.children.length !== 0;
|
||||
|
||||
const node =
|
||||
hasChildren && !hideChildrenInMenu ? (
|
||||
<a-sub-menu
|
||||
key={element?.name}
|
||||
v-slots={{
|
||||
icon,
|
||||
title: () => h(compile(t(element?.meta?.locale || ''))),
|
||||
}}
|
||||
>
|
||||
{element?.children ? travel(element?.children) : null}
|
||||
</a-sub-menu>
|
||||
) : (
|
||||
<a-menu-item
|
||||
key={element?.name}
|
||||
v-slots={{ icon }}
|
||||
onClick={() => goto(element)}
|
||||
>
|
||||
{t(element?.meta?.locale || '')}
|
||||
</a-menu-item>
|
||||
);
|
||||
nodes.push(node as never);
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
return travel(menuTree.value);
|
||||
};
|
||||
|
||||
// 返回 TSX 渲染函数(直接渲染菜单)
|
||||
return () => (
|
||||
<a-menu
|
||||
mode={topMenu.value ? 'horizontal' : 'vertical'}
|
||||
v-model:collapsed={collapsed.value}
|
||||
v-model:open-keys={openKeys.value}
|
||||
show-collapse-button={appStore.device !== 'mobile'}
|
||||
auto-open={true}
|
||||
selected-keys={selectedKey.value}
|
||||
auto-open-selected={true}
|
||||
level-indent={34}
|
||||
style="height: 100%;width:100%;"
|
||||
onCollapse={setCollapse}
|
||||
>
|
||||
{renderSubMenu()}
|
||||
</a-menu>
|
||||
);
|
||||
},
|
||||
});
|
||||
2
src/env.d.ts
vendored
2
src/env.d.ts
vendored
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ export default function usePermission() {
|
|||
const userStore = useUserStore();
|
||||
return {
|
||||
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
|
||||
return true
|
||||
return true;
|
||||
// 如果路由不需要认证,直接允许访问
|
||||
// if (!route.meta?.requiresAuth) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
|
||||
// // 如果用户没有权限数据,拒绝访问
|
||||
// if (!userStore.permission || userStore.permission.length === 0) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
|
||||
// // 检查用户是否有该路由的权限
|
||||
// return userStore.permission.some(
|
||||
// (p: any) =>
|
||||
|
|
@ -29,14 +29,15 @@ export default function usePermission() {
|
|||
if (!permissions || permissions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const cloneRouters = [..._routers];
|
||||
while (cloneRouters.length) {
|
||||
const firstElement = cloneRouters.shift();
|
||||
|
||||
|
||||
if (
|
||||
permissions.some(
|
||||
(p: any) => p.perm_key === firstElement.meta?.permissionKey && p.visible === 1
|
||||
(p: any) =>
|
||||
p.perm_key === firstElement.meta?.permissionKey && p.visible === 1
|
||||
)
|
||||
) {
|
||||
return { name: firstElement.name };
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import locale403 from '@/views/exception/403/locale/en-US';
|
|||
import locale404 from '@/views/exception/404/locale/en-US';
|
||||
import locale500 from '@/views/exception/500/locale/en-US';
|
||||
|
||||
|
||||
import localeSettings from './en-US/settings';
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import App from './App.vue';
|
|||
// https://arco.design/docs/designlab/use-theme-package
|
||||
import '@/assets/style/global.less';
|
||||
import '@/api/interceptor';
|
||||
|
||||
import '@/assets/style/index.css';
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(ArcoVue, {});
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export default function setupPermissionGuard(router: Router) {
|
|||
next();
|
||||
} else next(NOT_FOUND);
|
||||
} else {
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (permissionsAllow) next();
|
||||
else {
|
||||
const destination =
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ const RESULT: AppRouteRecordRaw = {
|
|||
path: '/result',
|
||||
name: 'result',
|
||||
component: DEFAULT_LAYOUT,
|
||||
redirect:'/result/success',
|
||||
redirect: '/result/success',
|
||||
meta: {
|
||||
locale: 'menu.result',
|
||||
icon: 'icon-check-circle',
|
||||
requiresAuth: true,
|
||||
order: 5,
|
||||
isFirst:true,
|
||||
isFirst: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ const USER: AppRouteRecordRaw = {
|
|||
path: '/user',
|
||||
name: 'user',
|
||||
component: DEFAULT_LAYOUT,
|
||||
redirect:'/user/info',
|
||||
redirect: '/user/info',
|
||||
meta: {
|
||||
locale: 'menu.user',
|
||||
icon: 'icon-user',
|
||||
requiresAuth: true,
|
||||
order: 7,
|
||||
isFirst:true,
|
||||
isFirst: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ const VISUALIZATION: AppRouteRecordRaw = {
|
|||
path: '/visualization',
|
||||
name: 'visualization',
|
||||
component: DEFAULT_LAYOUT,
|
||||
redirect:'/visualization/data-analysis',
|
||||
redirect: '/visualization/data-analysis',
|
||||
meta: {
|
||||
locale: 'menu.visualization',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-apps',
|
||||
order: 1,
|
||||
isFirst:true,
|
||||
isFirst: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const PROPERTY: AppRouteRecordRaw = {
|
|||
requiresAuth: true,
|
||||
icon: 'icon-list',
|
||||
order: 4,
|
||||
isFirst:true,
|
||||
isFirst: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -118,13 +118,55 @@ const SETTING: AppRouteRecordRaw = {
|
|||
{
|
||||
path: 'contract',
|
||||
name: 'system-setting-contract',
|
||||
component: () =>
|
||||
import('@/views/setting/system/contract-settings/index.vue'),
|
||||
redirect: '/setting/system/contract/setting',
|
||||
component: PageLayout,
|
||||
meta: {
|
||||
locale: 'setting.contract',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
hideChildrenInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'setting',
|
||||
name: 'contract-setting',
|
||||
component: () =>
|
||||
import('@/views/setting/system/contract-settings/index.vue'),
|
||||
meta: {
|
||||
locale: 'setting.contract',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
// 合同设置-合同文档模板设置-编辑
|
||||
{
|
||||
path: 'file-edit',
|
||||
name: 'system-setting-contract-file-edit',
|
||||
component: () =>
|
||||
import(
|
||||
'@/views/setting/system/contract-settings/contract-file-edit.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'setting.contract',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
// 合同设置-合同模板设置-编辑
|
||||
{
|
||||
path: 'template-edit',
|
||||
name: 'system-setting-contract-template-edit',
|
||||
component: () =>
|
||||
import(
|
||||
'@/views/setting/system/contract-settings/contract-template-edit.vue'
|
||||
),
|
||||
meta: {
|
||||
locale: 'setting.contract',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// 财务设置
|
||||
{
|
||||
|
|
|
|||
|
|
@ -413,3 +413,6 @@ export type ContractWordSettingListRes = HttpResponse<{
|
|||
contractWordSetting: ContractWordItem[];
|
||||
page?: PaginationParams & PaginationResult;
|
||||
}>;
|
||||
export type SettingContractFileInfoRes = HttpResponse<{
|
||||
contractWordSetting: CreateFileSettingParam;
|
||||
}>;
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
import axios, { AxiosError } from 'axios'
|
||||
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function isString(obj: any): obj is string {
|
|||
}
|
||||
|
||||
export function isNumber(obj: any): obj is number {
|
||||
return opt.call(obj) === '[object Number]' && obj === obj; // eslint-disable-line
|
||||
return opt.call(obj) === '[object Number]' && !Number.isNaN(obj);
|
||||
}
|
||||
|
||||
export function isRegExp(obj: any) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<div class="system-setting-contract-file">
|
||||
<Breadcrumb
|
||||
:items="['menu.setting', 'menu.setting.system', 'setting.contract']"
|
||||
/>
|
||||
<a-card class="full_height">
|
||||
<a-row :gutter="24">
|
||||
<a-col
|
||||
v-if="contractFileInfo && Object.keys(contractFileInfo).length"
|
||||
:span="7"
|
||||
>
|
||||
<icon-arrow-left /><span class="ml-[10px]">合同文档模板设置</span>
|
||||
<div
|
||||
class="mt-[24px] rounded-[6px] border-solid border-[#efefef] border-[1px] px-[12px] py-[20px]"
|
||||
>
|
||||
<h4 class="text-[16px] font-bold">{{ contractFileInfo?.name }}</h4>
|
||||
<p class="text-[14px] mt-[12px]"
|
||||
><span class="mr-[5px] text-[#101010]">适用范围</span
|
||||
>立思广州分公司</p
|
||||
>
|
||||
<p class="text-[14px] mt-[4px]"
|
||||
><span class="mr-[5px] text-[#101010]">适用项目</span
|
||||
>星河商务中心</p
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center mt-[24px]">
|
||||
<a-input-search class="w-[256px] h-[30px]" placeholder="搜索名称" />
|
||||
<div
|
||||
class="flex items-center border-[1px] border-solid border-[#165DFF] px-[4px] py-[6px] ml-[10px]"
|
||||
>
|
||||
<icon-google />
|
||||
<p class="ml-[5px] text-[#165DFF]">AI助手</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-tabs default-active-key="1" class="mt-[10px]">
|
||||
<a-tab-pane key="1" title="租赁条款"> 租赁条款 </a-tab-pane>
|
||||
<a-tab-pane key="2" title="物业条款"> w物业条款 </a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-col>
|
||||
<a-col :span="17">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-upload />
|
||||
</template>
|
||||
上传文档
|
||||
</a-button>
|
||||
<a-button class="ml-[10px]">
|
||||
<template #icon>
|
||||
<icon-cloud-download />
|
||||
</template>
|
||||
下载文档
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<p>2025-09-10 00:25:30更新</p>
|
||||
<a-button type="primary" class="ml-[10px]">
|
||||
<template #icon>
|
||||
<icon-storage />
|
||||
</template>
|
||||
保存
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- OnlyOffice 文档编辑器 -->
|
||||
<div class="onlyoffice-wrapper">
|
||||
<!-- <OnlyOfficeEditor v-if="contractFileInfo?.url" :document-url="contractFileInfo.url"
|
||||
:document-title="contractFileInfo.name || '合同文档'"
|
||||
:document-type="getDocumentType(contractFileInfo.url)" :height="'600px'" :editable="true"
|
||||
:mode="'edit'" @ready="onEditorReady" @error="onEditorError" @save="onDocumentSave"
|
||||
@close="onEditorClose" />
|
||||
<div v-else class="no-document">
|
||||
<a-empty description="暂无文档内容" />
|
||||
</div> -->
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { getWordSettingFromId } from '@/api/setting';
|
||||
import { CreateFileSettingParam } from '@/types/setting';
|
||||
|
||||
const contractFileInfo = ref<CreateFileSettingParam | null>(null);
|
||||
const route = useRoute();
|
||||
|
||||
// 获取文档数据
|
||||
const getWordSettingFromIdData = async () => {
|
||||
const id: any = route?.query?.id;
|
||||
const { code, data } = await getWordSettingFromId(id);
|
||||
if (code === 200 && data) {
|
||||
const { contractWordSetting } = data;
|
||||
contractFileInfo.value = contractWordSetting || null;
|
||||
contractFileInfo.value.url =
|
||||
'http://oss.kola.work/documents%2F20250910%2F%E5%90%88%E5%90%8C%E6%A8%A1%E7%89%88.docx?Expires=1757479342&OSSAccessKeyId=LTAI5tDDNxXnZq7QcpZEf6hR&Signature=ydBzbF2yCyRWZ0G52s%2FPr%2BFIUzo%3D';
|
||||
}
|
||||
};
|
||||
|
||||
// 根据文件 URL 获取文档类型
|
||||
const getDocumentType = (url: string): 'word' | 'excel' | 'powerpoint' => {
|
||||
if (!url) return 'word';
|
||||
|
||||
const extension = url.toLowerCase().split('.').pop();
|
||||
switch (extension) {
|
||||
case 'xlsx':
|
||||
case 'xls':
|
||||
return 'excel';
|
||||
case 'pptx':
|
||||
case 'ppt':
|
||||
return 'powerpoint';
|
||||
case 'docx':
|
||||
case 'doc':
|
||||
default:
|
||||
return 'word';
|
||||
}
|
||||
};
|
||||
|
||||
// OnlyOffice 编辑器事件处理
|
||||
const onEditorReady = () => {
|
||||
console.log('OnlyOffice 编辑器已准备就绪');
|
||||
Message.success('文档编辑器加载完成');
|
||||
};
|
||||
|
||||
const onEditorError = (error: string) => {
|
||||
console.error('OnlyOffice 编辑器错误:', error);
|
||||
Message.error(`编辑器错误: ${error}`);
|
||||
};
|
||||
|
||||
const onDocumentSave = (data: any) => {
|
||||
console.log('文档保存事件:', data);
|
||||
Message.success('文档已保存');
|
||||
// 这里可以添加保存后的处理逻辑,比如更新服务器数据
|
||||
};
|
||||
|
||||
const onEditorClose = () => {
|
||||
console.log('编辑器关闭');
|
||||
Message.info('编辑器已关闭');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getWordSettingFromIdData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.onlyoffice-wrapper {
|
||||
margin-top: 16px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.no-document {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.system-setting-contract-file {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.full_height {
|
||||
height: calc(100vh - 120px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.arco-row) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-col) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -61,7 +61,6 @@
|
|||
import { Message } from '@arco-design/web-vue';
|
||||
import useDataTable from '@/composables/useDataTable';
|
||||
import {
|
||||
getWordSettingFromId,
|
||||
delWordSetting,
|
||||
getWordSettingList,
|
||||
disableWordSetting,
|
||||
|
|
@ -72,7 +71,7 @@
|
|||
import { camelToSnake } from '@/utils/caseConvert';
|
||||
// import contractNumberFormDrawer from './drawer/contract-number-form-drawer.vue';
|
||||
import contractFileForm from './modal/contract-file-form.vue';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
const {
|
||||
data: renderData,
|
||||
columns: projectColumn,
|
||||
|
|
@ -91,7 +90,7 @@
|
|||
initialPageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const contractWordList = ref<ContractWordItem[]>([]);
|
||||
const departmentListData = ref<DepartmentItem[]>([]);
|
||||
const showModal = ref<boolean>(false);
|
||||
|
|
@ -200,26 +199,16 @@
|
|||
// 处理错误
|
||||
}
|
||||
};
|
||||
const getWordSettingFromIdData = async (id: number) => {
|
||||
const { code, data } = await getWordSettingFromId(id);
|
||||
if (code === 200 && data) {
|
||||
const { contractCodeSetting } = data;
|
||||
currentNumberInfo.value = contractCodeSetting || null;
|
||||
currentNumberInfo.value.arrange = camelToSnake(
|
||||
currentNumberInfo.value.arrange
|
||||
);
|
||||
}
|
||||
};
|
||||
const handleAddContractNumber = () => {
|
||||
// currentNumberInfo.value = null;
|
||||
isEdit.value = false;
|
||||
showModal.value = true;
|
||||
};
|
||||
const handleEditContractNumber = async (id: number) => {
|
||||
await getWordSettingFromIdData(id);
|
||||
isEdit.value = true;
|
||||
// currentNumberInfo.value = info
|
||||
showModal.value = true;
|
||||
router.push({
|
||||
name: 'system-setting-contract-file-edit',
|
||||
query: { id },
|
||||
});
|
||||
};
|
||||
// 初始化数据
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,709 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="
|
||||
currentContractTemplateInfo &&
|
||||
Object.keys(currentContractTemplateInfo).length
|
||||
"
|
||||
class="system-setting-contract-file pr-[20px]"
|
||||
>
|
||||
<Breadcrumb
|
||||
:items="['menu.setting', 'menu.setting.system', 'setting.contract']"
|
||||
/>
|
||||
<div class="flex items-center justify-between cursor-pointer mb-[20px]">
|
||||
<div @click="router.go(-1)">
|
||||
<icon-arrow-left /> <span class="ml-[10px]">合同文档模板设置</span>
|
||||
</div>
|
||||
<a-button type="primary" @click="handleSubmit">保存</a-button>
|
||||
</div>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="7">
|
||||
<a-card class="full_height rounded-[5px]">
|
||||
<div class="pb-[10px] border-b-[1px] border-b-solid border-[#f0f0f0]">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-[#000]">{{ currentContractTemplateInfo.name }}</p>
|
||||
<p class="cursor-pointer" @click="handleBaseInfoEdit"
|
||||
><icon-edit
|
||||
/></p>
|
||||
</div>
|
||||
<a-tag class="mt-[5px]" color="green">{{
|
||||
currentContractTemplateInfo.contractType
|
||||
}}</a-tag>
|
||||
<p class="text-[#000] mt-[5px]">
|
||||
<span class="text-[#999999] mr-[5px]">适用范围</span>
|
||||
{{ currentContractTemplateInfo.departmentName }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="py-[10px]">
|
||||
<h3 class="text-[16px] text-[#505968]">基本配置</h3>
|
||||
<p
|
||||
:class="[
|
||||
'cursor-pointer mt-[8px] px-[10px] h-[48px] leading-[48px] text-left text-[14px]',
|
||||
'rounded-[2px] border-[1px] border-solid border-[#f8f8f8]',
|
||||
{ isActive: currentId === 0 },
|
||||
]"
|
||||
@click="handleChoose(0, '基本信息')"
|
||||
>
|
||||
基本信息</p
|
||||
>
|
||||
<div class="flex items-center justify-between mt-[8px]">
|
||||
<h3 class="text-[16px] text-[#505968]">条款配置</h3>
|
||||
<p class="cursor-pointer">
|
||||
<a-popover trigger="click" position="br">
|
||||
<icon-plus />
|
||||
<template #content>
|
||||
<ul>
|
||||
<li
|
||||
v-for="item in allTermsList"
|
||||
:key="item.id"
|
||||
class="cursor-pointer px-[10px] py-[5px] border-b-[1px] border-b-solid border-[#f0f0f0]"
|
||||
@click="handleAddTerms(item.id)"
|
||||
>
|
||||
{{ item.name }}</li
|
||||
>
|
||||
</ul>
|
||||
</template>
|
||||
</a-popover>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="item in currentContractTemplateInfo.terms"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'cursor-pointer mt-[8px] px-[10px] h-[48px] leading-[48px] text-left text-[14px]',
|
||||
'rounded-[2px] border-[1px] border-solid border-[#f8f8f8]',
|
||||
{ isActive: currentId === item.id },
|
||||
]"
|
||||
@click="handleChoose(item.id, item.name)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<contractTemplateForm
|
||||
v-model:visible="showModal"
|
||||
:is-edit="true"
|
||||
:department-list-data="departmentListData"
|
||||
:record="currentContractTemplateInfo"
|
||||
@submitted="getTemplateSettingFromIdData"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="17">
|
||||
<a-card class="full_height rounded-[5px]">
|
||||
<div class="flex items-center justify-between mb-[10px]">
|
||||
<p class="flex items-center">{{ title }}</p>
|
||||
<a-button
|
||||
v-if="currentId > 0"
|
||||
type="text"
|
||||
status="danger"
|
||||
@click="handleDeleteTerm"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
<template #default>删除条款</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<!-- 基本信息内容 -->
|
||||
<div v-if="currentId === 0" class="mt-[5px]">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="baseInfoForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<!-- 合同编号模板 -->
|
||||
<a-form-item
|
||||
label="合同编号模板"
|
||||
field="contractCodeSettingId"
|
||||
required
|
||||
>
|
||||
<a-select
|
||||
v-model="baseInfoForm.contractCodeSettingId"
|
||||
placeholder="请选择合同编号模板"
|
||||
@change="chooseCodeSettingId"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in codeSettingList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<div
|
||||
v-if="currentContractTemplateInfo.contractCode"
|
||||
class="p-[10px] bg-[#eaf3fe]"
|
||||
>
|
||||
合同编号预览:{{
|
||||
currentContractTemplateInfo.contractCode
|
||||
}}</div
|
||||
>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<!-- 合同类型 -->
|
||||
<a-form-item label="合同类型" field="type" required>
|
||||
<ul class="options">
|
||||
<li
|
||||
v-for="item in contractTaxMap"
|
||||
:key="item.type"
|
||||
:class="[
|
||||
'subItem',
|
||||
{ isActive: item.type === baseInfoForm.type },
|
||||
]"
|
||||
@click="chooseTaxType(item.type)"
|
||||
>
|
||||
{{ item.text }}</li
|
||||
>
|
||||
</ul>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<p class="my-[20px] font-bold text-[15px] text-[#101010]"
|
||||
>其他信息(选填)</p
|
||||
>
|
||||
<a-form-item label="合同标签" field="tags">
|
||||
<a-select
|
||||
v-model="baseInfoForm.tags"
|
||||
multiple
|
||||
placeholder="请选择合同标签"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in contractTagsList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" field="remark">
|
||||
<a-textarea
|
||||
v-model="baseInfoForm.remark"
|
||||
placeholder="输入合同备注"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<template v-else>
|
||||
<a-tag
|
||||
v-for="item of termTabList"
|
||||
:key="item.key"
|
||||
:style="{ color: item.isChoose === 1 ? '#165DFF' : '' }"
|
||||
:color="item.isChoose === 1 ? '#eaf3fe' : ''"
|
||||
:class="[
|
||||
'mr-[10px] mb-[10px] cursor-pointer text-[#1D2129]',
|
||||
{ 'text-[#165DFF]': item.isChoose == 1 },
|
||||
]"
|
||||
@click="handleToggleTerm(item)"
|
||||
>
|
||||
{{ item.title }}
|
||||
<icon-close v-if="item.isChoose === 1" />
|
||||
</a-tag>
|
||||
<a-tag class="cursor-pointer mb-[10px]" @click="handleAddTerm">
|
||||
<icon-plus />
|
||||
优惠条款
|
||||
</a-tag>
|
||||
<div class="mt-[20px]">
|
||||
<bond
|
||||
v-if="isBondShown"
|
||||
ref="bondRef"
|
||||
:title="'保证金条款(租金保证金)'"
|
||||
/>
|
||||
<incremental
|
||||
v-if="isIncrementalShown"
|
||||
ref="incrementalRef"
|
||||
:title="'递增条款'"
|
||||
/>
|
||||
<discount
|
||||
v-for="(d, di) in discountChosen"
|
||||
:key="d.key"
|
||||
:ref="(el) => setDiscountRef(el, di)"
|
||||
:title="d.title"
|
||||
/>
|
||||
<lateFee
|
||||
v-if="isLateFeeShown"
|
||||
ref="lateFeeRef"
|
||||
:title="'滞纳金条款'"
|
||||
/>
|
||||
<rentSetting
|
||||
ref="rentSettingRef"
|
||||
:term-setting-info="termSettingInfo"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, reactive, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import {
|
||||
getTemplateSettingFromId,
|
||||
getTermsSettingFromId,
|
||||
getDepartmentList,
|
||||
getAllCodeSettingList,
|
||||
getContractTagsList,
|
||||
updateTemplateSetting,
|
||||
getAllTermsSettingList,
|
||||
} from '@/api/setting';
|
||||
import contractTemplateForm from './modal/contract-template-form.vue';
|
||||
import { DepartmentItem } from '@/types/setting';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import bond from './terms/bond.vue';
|
||||
import incremental from './terms/incremental.vue';
|
||||
import discount from './terms/discount.vue';
|
||||
import lateFee from './terms/late-fee.vue';
|
||||
import rentSetting from './terms/rent-setting.vue';
|
||||
const currentContractTemplateInfo = ref<any>(null);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const showModal = ref<boolean>(false);
|
||||
const title = ref<string>('基本信息');
|
||||
const currentId = ref<number>(0);
|
||||
const departmentListData = ref<DepartmentItem[]>([]);
|
||||
const contractTaxMap = [
|
||||
// 1是含税2不含税
|
||||
{ type: 1, text: '含税合同' },
|
||||
{ type: 2, text: '不含税合同' },
|
||||
];
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
id: null,
|
||||
contractCodeSettingId: null,
|
||||
saveType: 2, // 保存类别:1 名称与适用范围数据,2 基础信息数据,3 条款数据)
|
||||
tags: [],
|
||||
remark: '',
|
||||
type: 1,
|
||||
};
|
||||
const codeSettingList = ref<any>([]);
|
||||
const baseInfoForm = reactive<any>({ ...defaultForm });
|
||||
const allTermsList = ref<any>([]);
|
||||
// refs for terms components
|
||||
const rentSettingRef = ref();
|
||||
const bondRef = ref();
|
||||
const incrementalRef = ref();
|
||||
const lateFeeRef = ref();
|
||||
const discountRefs = ref<any[]>([]);
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
contractCodeSettingId: [
|
||||
{ required: true, message: '合同编号模板不能为空' },
|
||||
],
|
||||
type: [{ required: true, message: '请选择合同类型' }],
|
||||
});
|
||||
const termSettingInfo = ref<any>({});
|
||||
const termTabList = ref([
|
||||
{
|
||||
key: 'bond',
|
||||
title: '保证金条款',
|
||||
isChoose: 1,
|
||||
type: 'bond',
|
||||
},
|
||||
{
|
||||
key: 'incremental',
|
||||
title: '递增条款',
|
||||
isChoose: 1,
|
||||
type: 'incremental',
|
||||
},
|
||||
{
|
||||
key: 'discount-0',
|
||||
title: '单价折扣',
|
||||
isChoose: 1,
|
||||
type: 'unitDiscount',
|
||||
},
|
||||
{
|
||||
key: 'discount',
|
||||
title: '优惠条款',
|
||||
isChoose: 1,
|
||||
type: 'discount',
|
||||
},
|
||||
{
|
||||
key: 'lateFee',
|
||||
title: '滞纳金条款',
|
||||
isChoose: 1,
|
||||
type: 'lateFee',
|
||||
},
|
||||
]);
|
||||
const contractTagsList = ref<any>([]);
|
||||
const getTemplateSettingFromIdData = async () => {
|
||||
const id: any = route?.query?.id;
|
||||
const { code, data } = await getTemplateSettingFromId(id);
|
||||
if (code === 200 && data) {
|
||||
const { contractTemplateSetting } = data;
|
||||
currentContractTemplateInfo.value = contractTemplateSetting || null;
|
||||
Object.keys(baseInfoForm).forEach((key) => {
|
||||
// const value = baseInfoForm[key];
|
||||
if (key === 'tags') {
|
||||
baseInfoForm.tags = contractTemplateSetting.tags
|
||||
.map((i) => i.id)
|
||||
.filter(Boolean);
|
||||
} else {
|
||||
baseInfoForm[key] = contractTemplateSetting[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
// 部门列表
|
||||
const getDepartmentListData = async () => {
|
||||
try {
|
||||
const { code, data } = await getDepartmentList();
|
||||
if (code === 200) {
|
||||
departmentListData.value = data.departmentList;
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('获取部门列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 由 termTabList 计算可见的优惠条款列表
|
||||
const discountChosen = computed<any[]>(() =>
|
||||
termTabList.value.filter(
|
||||
(i: any) => i.type === 'discount' && i.isChoose === 1
|
||||
)
|
||||
);
|
||||
// 组件显隐由接口开关 + 选择状态共同决定
|
||||
let toNum = (v: any) => Number(v || 0);
|
||||
|
||||
const isBondShown = computed(
|
||||
() =>
|
||||
toNum(termSettingInfo.value?.isBond) === 1 &&
|
||||
termTabList.value.some((i: any) => i.type === 'bond' && i.isChoose === 1)
|
||||
);
|
||||
const isIncrementalShown = computed(
|
||||
() =>
|
||||
toNum(termSettingInfo.value?.isIncremental) === 1 &&
|
||||
termTabList.value.some(
|
||||
(i: any) => i.type === 'incremental' && i.isChoose === 1
|
||||
)
|
||||
);
|
||||
const isLateFeeShown = computed(
|
||||
() =>
|
||||
toNum(termSettingInfo.value?.isLateFee) === 1 &&
|
||||
termTabList.value.some(
|
||||
(i: any) => i.type === 'lateFee' && i.isChoose === 1
|
||||
)
|
||||
);
|
||||
async function getTermsSettingFromIdData(id) {
|
||||
const { code, data } = await getTermsSettingFromId(id);
|
||||
if (code === 200) {
|
||||
termSettingInfo.value = data?.contractTermsSetting ?? {};
|
||||
// 根据后端开关更新标签显示状态(例如:isBond === 2 时,保证金标签置为未选中)
|
||||
const next = (termTabList.value || []).map((t) => ({ ...t }));
|
||||
const bondIdx = next.findIndex((t) => t.type === 'bond');
|
||||
if (bondIdx > -1) {
|
||||
next[bondIdx].isChoose =
|
||||
toNum(termSettingInfo.value?.isBond) === 1 ? 1 : 2;
|
||||
}
|
||||
// 保持其他标签不变;如需扩展递增、滞纳金可在此追加
|
||||
termTabList.value = next;
|
||||
}
|
||||
}
|
||||
const handleChoose = (id: number, str: string) => {
|
||||
if (id === currentId.value) return;
|
||||
currentId.value = id;
|
||||
title.value = str;
|
||||
getTermsSettingFromIdData(id);
|
||||
};
|
||||
const handleBaseInfoEdit = () => {
|
||||
showModal.value = true;
|
||||
};
|
||||
const getAllCodeSettingListData = async () => {
|
||||
const { code, data } = await getAllCodeSettingList();
|
||||
if (code === 200) {
|
||||
codeSettingList.value = data?.contractCodeSetting || [];
|
||||
}
|
||||
};
|
||||
const chooseCodeSettingId = (id) => {
|
||||
const info = codeSettingList.value.find((i) => i.id === id);
|
||||
if (info) {
|
||||
currentContractTemplateInfo.value.contractCode = info?.code;
|
||||
}
|
||||
};
|
||||
const chooseTaxType = (type) => {
|
||||
baseInfoForm.type = type;
|
||||
};
|
||||
const getContractTagsListData = async () => {
|
||||
const { code, data } = await getContractTagsList();
|
||||
if (code === 200 && data) {
|
||||
contractTagsList.value = data?.contractTagsSettingList;
|
||||
}
|
||||
};
|
||||
const submitBaseInfo = async () => {
|
||||
await formRef.value?.validate();
|
||||
baseInfoForm.id = Number(route?.query?.id);
|
||||
const { code, message } = await updateTemplateSetting(baseInfoForm);
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
}
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
if (currentId.value === 0) {
|
||||
await submitBaseInfo();
|
||||
} else {
|
||||
// 先统一执行可见子表单校验;任一失败则中断
|
||||
let hasValidationError = false;
|
||||
if ((rentSettingRef as any)?.value?.validate) {
|
||||
try {
|
||||
await (rentSettingRef as any).value.validate();
|
||||
} catch (e) {
|
||||
hasValidationError = true;
|
||||
}
|
||||
}
|
||||
if (isBondShown.value && (bondRef as any)?.value?.validate) {
|
||||
try {
|
||||
await (bondRef as any).value.validate();
|
||||
} catch (e) {
|
||||
hasValidationError = true;
|
||||
}
|
||||
}
|
||||
if (
|
||||
isIncrementalShown.value &&
|
||||
(incrementalRef as any)?.value?.validate
|
||||
) {
|
||||
try {
|
||||
await (incrementalRef as any).value.validate();
|
||||
} catch (e) {
|
||||
hasValidationError = true;
|
||||
}
|
||||
}
|
||||
if (isLateFeeShown.value && (lateFeeRef as any)?.value?.validate) {
|
||||
try {
|
||||
await (lateFeeRef as any).value.validate();
|
||||
} catch (e) {
|
||||
hasValidationError = true;
|
||||
}
|
||||
}
|
||||
const validationPromises = (discountRefs.value || [])
|
||||
.filter((r) => r?.validate)
|
||||
.map((r) =>
|
||||
r.validate().catch(() => {
|
||||
hasValidationError = true;
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(validationPromises);
|
||||
|
||||
if (hasValidationError) return;
|
||||
|
||||
// 校验通过后再收集数据
|
||||
const rentSettingData = (rentSettingRef as any)?.value?.getData?.() || {};
|
||||
const bondData = (bondRef as any)?.value?.getData?.() || {};
|
||||
const incrementalData = (incrementalRef as any)?.value?.getData?.() || {};
|
||||
const discountData = (discountRefs.value || [])
|
||||
.map((r: any) => r?.getData?.() || {})
|
||||
.filter((x: any) => x && Object.keys(x).length);
|
||||
const lateFeeData = (lateFeeRef as any)?.value?.getData?.() || {};
|
||||
|
||||
// 非空校验(仅校验当前显示/选中的条款)
|
||||
const isEmptyValue = (v: any) =>
|
||||
v === undefined ||
|
||||
v === null ||
|
||||
(typeof v === 'string' && v.trim() === '');
|
||||
const validateObject = (obj: any, label: string): string[] => {
|
||||
if (!obj || typeof obj !== 'object') return [`${label}不能为空`];
|
||||
if (Object.keys(obj).length === 0) return [`${label}不能为空`];
|
||||
const errs: string[] = [];
|
||||
Object.keys(obj).forEach((k) => {
|
||||
const val = obj[k];
|
||||
// 数值 0 视为有效
|
||||
if (typeof val === 'number') return;
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length === 0) errs.push(`${label}存在未填写项`);
|
||||
} else if (isEmptyValue(val)) {
|
||||
errs.push(`${label}存在未填写项`);
|
||||
}
|
||||
});
|
||||
return errs;
|
||||
};
|
||||
const errors: string[] = [];
|
||||
// 租金/租期条款固定显示,必校验
|
||||
errors.push(...validateObject(rentSettingData, '租期条款'));
|
||||
// 保证金
|
||||
if (isBondShown.value)
|
||||
errors.push(...validateObject(bondData, '保证金条款'));
|
||||
// 递增
|
||||
if (isIncrementalShown.value)
|
||||
errors.push(...validateObject(incrementalData, '递增条款'));
|
||||
// 优惠(多个)
|
||||
if (discountChosen.value.length > 0) {
|
||||
if (discountData.length === 0) {
|
||||
errors.push('优惠条款存在未填写项');
|
||||
} else {
|
||||
discountData.forEach((d: any, idx: number) => {
|
||||
errors.push(...validateObject(d, `优惠条款${idx + 1}`));
|
||||
});
|
||||
}
|
||||
}
|
||||
// 滞纳金
|
||||
if (isLateFeeShown.value)
|
||||
errors.push(...validateObject(lateFeeData, '滞纳金条款'));
|
||||
|
||||
if (errors.length > 0) {
|
||||
Message.error(errors[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTerm =
|
||||
(currentContractTemplateInfo.value?.terms || []).find(
|
||||
(t: any) => t.id === currentId.value
|
||||
) || {};
|
||||
|
||||
const payload: any = {
|
||||
saveType: 3,
|
||||
id: Number(route?.query?.id),
|
||||
name: currentContractTemplateInfo.value?.name,
|
||||
departmentId: currentContractTemplateInfo.value?.departmentId,
|
||||
type: baseInfoForm.type,
|
||||
contractCodeSettingId: baseInfoForm.contractCodeSettingId,
|
||||
tags: baseInfoForm.tags,
|
||||
remark: baseInfoForm.remark,
|
||||
term: {
|
||||
id: currentTerm?.id,
|
||||
type: 1,
|
||||
contractTermsSettingId: currentTerm?.id,
|
||||
leaseTerm: rentSettingData,
|
||||
bondTerm: bondData,
|
||||
incrementalTerm: incrementalData,
|
||||
discountTerm: discountData,
|
||||
lateFeeTerm: lateFeeData,
|
||||
remarkTerm: { remark: '' },
|
||||
},
|
||||
};
|
||||
|
||||
updateTemplateSetting(payload).then(({ code, message }) => {
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
const getAllTermsSettingListData = async () => {
|
||||
const { code, data } = await getAllTermsSettingList();
|
||||
if (code === 200) {
|
||||
allTermsList.value = data?.contractTermsSetting || [];
|
||||
}
|
||||
};
|
||||
function handleAddTerms(id: number) {
|
||||
const allList = allTermsList.value || [];
|
||||
const found = allList.find((t: any) => t.id === id);
|
||||
if (!found) return;
|
||||
const list = currentContractTemplateInfo.value?.terms || [];
|
||||
const exists = list.some((t: any) => t.id === id);
|
||||
if (!exists) {
|
||||
// 仅插入必要字段,保持与现有结构一致
|
||||
list.push({ id: found.id, name: found.name });
|
||||
currentContractTemplateInfo.value.terms = [...list];
|
||||
}
|
||||
// 切换到新添加/已存在的条款并拉取其配置
|
||||
currentId.value = id;
|
||||
title.value = found.name;
|
||||
getTermsSettingFromIdData(id);
|
||||
}
|
||||
|
||||
function handleDeleteTerm() {
|
||||
const id = currentId.value;
|
||||
if (!id) return;
|
||||
const terms = currentContractTemplateInfo.value?.terms || [];
|
||||
const nextTerms = terms.filter((t: any) => t.id !== id);
|
||||
currentContractTemplateInfo.value.terms = nextTerms;
|
||||
|
||||
// 选择下一个可编辑的目标:优先剩余的第一项,否则回到基本信息
|
||||
if (nextTerms.length > 0) {
|
||||
currentId.value = nextTerms[0].id;
|
||||
title.value = nextTerms[0].name;
|
||||
getTermsSettingFromIdData(nextTerms[0].id);
|
||||
} else {
|
||||
currentId.value = 0;
|
||||
title.value = '基本信息';
|
||||
}
|
||||
}
|
||||
function handleToggleTerm(info: any) {
|
||||
if (info?.type === 'discount') {
|
||||
const discounts = termTabList.value.filter(
|
||||
(i: any) => i.type === 'discount'
|
||||
);
|
||||
if (discounts.length > 1) {
|
||||
// 删除重复的优惠条款,仅保留第一条
|
||||
const firstKey = discounts[0].key;
|
||||
termTabList.value = termTabList.value.filter(
|
||||
(i: any) => i.type !== 'discount' || i.key === firstKey
|
||||
);
|
||||
// 强制保留项为选中
|
||||
const keep = termTabList.value.find((i: any) => i.key === firstKey);
|
||||
if (keep) keep.isChoose = 1;
|
||||
} else {
|
||||
// 只有一条时正常切换选中状态
|
||||
info.isChoose = info.isChoose === 1 ? 2 : 1;
|
||||
}
|
||||
} else {
|
||||
info.isChoose = info.isChoose === 1 ? 2 : 1;
|
||||
}
|
||||
}
|
||||
function handleAddTerm() {
|
||||
const idx = termTabList.value.filter(
|
||||
(i: any) => i.type === 'discount'
|
||||
).length;
|
||||
const key = `discount-${Date.now()}-${idx}`;
|
||||
termTabList.value.push({
|
||||
key,
|
||||
title: `优惠条款${idx + 1}`,
|
||||
isChoose: 1,
|
||||
type: 'discount',
|
||||
});
|
||||
}
|
||||
|
||||
const setDiscountRef = (el: any, index: number) => {
|
||||
if (el) discountRefs.value[index] = el;
|
||||
};
|
||||
onMounted(() => {
|
||||
getTemplateSettingFromIdData();
|
||||
getDepartmentListData();
|
||||
getAllCodeSettingListData();
|
||||
getContractTagsListData();
|
||||
getAllTermsSettingListData();
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.isActive {
|
||||
background-color: #eaf3fe;
|
||||
border-color: #73a5f1;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
:deep(.arco-popover-popup-content) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.arco-tag .arco-tag-custom-color) {
|
||||
color: #165dff !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -48,20 +48,19 @@
|
|||
</DataTable>
|
||||
<contractTemplateForm
|
||||
v-model:visible="showModal"
|
||||
:is-edit="false"
|
||||
:department-list-data="departmentListData"
|
||||
:is-edit="isEdit"
|
||||
@submitted="getTemplateSettingListData"
|
||||
/>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { TableColumnData } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useDataTable from '@/composables/useDataTable';
|
||||
import {
|
||||
getTemplateSettingFromId,
|
||||
delTemplateSetting,
|
||||
getTemplateSettingList,
|
||||
getDepartmentList,
|
||||
|
|
@ -69,10 +68,8 @@
|
|||
} from '@/api/setting';
|
||||
import { ContractTemplateSettingItem, DepartmentItem } from '@/types/setting';
|
||||
import { UserStatus } from '@/types/comm';
|
||||
import { camelToSnake } from '@/utils/caseConvert';
|
||||
// import contractNumberFormDrawer from './drawer/contract-number-form-drawer.vue';
|
||||
import contractTemplateForm from './modal/contract-template-form.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const {
|
||||
data: renderData,
|
||||
columns: projectColumn,
|
||||
|
|
@ -96,7 +93,7 @@
|
|||
const departmentListData = ref<DepartmentItem[]>([]);
|
||||
const showModal = ref<boolean>(false);
|
||||
const isEdit = ref<boolean>(false);
|
||||
const currentNumberInfo = ref<any>(null);
|
||||
const currentContractTemplateInfo = ref<any>(null);
|
||||
// 列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
|
|
@ -201,26 +198,17 @@
|
|||
// 处理错误
|
||||
}
|
||||
};
|
||||
const getTemplateSettingFromIdData = async (id: number) => {
|
||||
const { code, data } = await getTemplateSettingFromId(id);
|
||||
if (code === 200 && data) {
|
||||
const { contractCodeSetting } = data;
|
||||
currentNumberInfo.value = contractCodeSetting || null;
|
||||
currentNumberInfo.value.arrange = camelToSnake(
|
||||
currentNumberInfo.value.arrange
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddContractNumber = () => {
|
||||
// currentNumberInfo.value = null;
|
||||
currentContractTemplateInfo.value = null;
|
||||
isEdit.value = false;
|
||||
showModal.value = true;
|
||||
};
|
||||
const handleEditContractNumber = async (id: number) => {
|
||||
await getTemplateSettingFromIdData(id);
|
||||
isEdit.value = true;
|
||||
// currentNumberInfo.value = info
|
||||
showModal.value = true;
|
||||
router.push({
|
||||
name: 'system-setting-contract-template-edit',
|
||||
query: { id },
|
||||
});
|
||||
};
|
||||
// 初始化数据
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,313 @@
|
|||
<template>
|
||||
<a-drawer
|
||||
:width="650"
|
||||
:visible="visible"
|
||||
title="租赁条款租期设置"
|
||||
unmount-on-close
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<p class="title">计费相关</p>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="付款周期" field="payCycle" required>
|
||||
<a-input-number
|
||||
v-model="form.payCycle"
|
||||
append="月1付"
|
||||
:min="1"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="税率" field="taxRate" required>
|
||||
<a-input-number
|
||||
v-model="form.taxRate"
|
||||
append="%"
|
||||
:min="0"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="计费模式" field="chargeType" required>
|
||||
<a-select v-model="form.chargeType">
|
||||
<a-option :value="1">按月计算租金</a-option>
|
||||
<a-option :value="2">按实际天数计算租金</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="合同单价默认单位" field="priceUnit" required>
|
||||
<a-select v-model="form.priceUnit">
|
||||
<a-option
|
||||
v-for="item in priceUnits"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.text }}</a-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item v-if="form.chargeType === 1">
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full">
|
||||
<icon-exclamation-circle-fill style="color: #2d5cf6" />
|
||||
按月计算租金公式=合同单价 x 合同面积
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="每天单价算法(适合不足月租期计费)"
|
||||
field="dayPriceAlgorithm"
|
||||
required
|
||||
>
|
||||
<a-select v-model="form.dayPriceAlgorithm">
|
||||
<a-option :value="1">按当前年天数换算</a-option>
|
||||
<a-option :value="2">按当前月天数换算</a-option>
|
||||
<a-option :value="3">按自定义每天单价</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="每天单价" field="dayPrice" required>
|
||||
<a-input-number v-model="form.dayPrice" :min="0.1">
|
||||
<template #append>
|
||||
<a-select v-model="form.dayPriceUnit" style="width: 100px">
|
||||
<a-option
|
||||
v-for="item in priceUnits"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.text }}</a-option
|
||||
>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col v-if="form.dayPrice && form.dayPriceUnit" :span="24">
|
||||
<a-form-item>
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full">
|
||||
<icon-exclamation-circle-fill style="color: #2d5cf6" />
|
||||
每天单价算法公式 = {{ dayPriceCalc }} x 合同面积
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="计算精度" field="calculationAccuracy" required>
|
||||
<a-select v-model="form.calculationAccuracy">
|
||||
<a-option :value="1"
|
||||
>每步计算进行精确计算,直至计算结果。</a-option
|
||||
>
|
||||
<a-option :value="2">每步计算保留2位小数,直至计算结果</a-option>
|
||||
<a-option :value="2">每步计算保留1位小数,直至计算结果</a-option>
|
||||
<a-option :value="2">每步计算不保留小数,直至计算结果</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="每期账单小数点" field="billAccuracy" required>
|
||||
<a-select v-model="form.billAccuracy">
|
||||
<a-option :value="1">2位小数</a-option>
|
||||
<a-option :value="2">1位小数</a-option>
|
||||
<a-option :value="2">不保留</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<p class="title">租期相关</p>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-item label="租期划分模式" field="partitionMode" required>
|
||||
<a-select v-model="form.partitionMode">
|
||||
<a-option :value="1"
|
||||
>按自然月划分租期(“周期开始月”月首日到“后期结束月”月末日)</a-option
|
||||
>
|
||||
<a-option :value="2">按起止日划分租期</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full">
|
||||
<icon-exclamation-circle-fill style="color: #2d5cf6" />
|
||||
按自然月划分租期,如2025-10-01 ~ 2025-10-31
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="首月不足月租期处理"
|
||||
field="firstLessMonthType"
|
||||
required
|
||||
>
|
||||
<a-select v-model="form.firstLessMonthType">
|
||||
<a-option :value="1">独立为一个租期</a-option>
|
||||
<a-option :value="2">作为完整月来划分租期</a-option>
|
||||
<a-option :value="3">与下一租期合并为一个租期</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full">
|
||||
<icon-exclamation-circle-fill style="color: #2d5cf6" />
|
||||
与下一租期合并为一个租期,如:2月1付,2025-07-16 ~
|
||||
2025-09-30为一期账单
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="免租期、装修期租期处理"
|
||||
field="otherLeaseProcessing"
|
||||
required
|
||||
>
|
||||
<a-select v-model="form.otherLeaseProcessing">
|
||||
<a-option :value="1">免租期计入租期,装修期计入租期</a-option>
|
||||
<a-option :value="2">免租期计入租期,装修期不计入租期</a-option>
|
||||
<a-option :value="3">免租期不计入租期,装修期计入租期</a-option>
|
||||
<a-option :value="4">免租期不计入租期,装修期不计入租期</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="bg-[#eaf3fe] p-[10px] w-full flex items-center">
|
||||
<p> <icon-exclamation-circle-fill style="color: #2d5cf6" /></p>
|
||||
<div class="ml-[2px]">
|
||||
<p
|
||||
>计入租期:租期从免租/装修期起始时间开始算,虽免租但算在合同期内</p
|
||||
>
|
||||
<p
|
||||
>不计入租期:租期从免租/装修期结束时间开始算,为租期之外赠送的时间</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
const priceUnits = [
|
||||
{ value: 1, text: '元/㎡*月' },
|
||||
{ value: 2, text: '元/㎡*天' },
|
||||
{ value: 3, text: '元/天' },
|
||||
{ value: 4, text: '元/月' },
|
||||
{ value: 5, text: '元/年' },
|
||||
];
|
||||
const priceUnitMap = new Map([
|
||||
[1, '元/㎡*月'],
|
||||
[2, '元/㎡*天'],
|
||||
[3, '元/天'],
|
||||
[4, '元/月'],
|
||||
[5, '元/年'],
|
||||
]);
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
record?: any | null;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'submitted', val: any): void;
|
||||
}>();
|
||||
const isLoading = ref<boolean>(false);
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
payCycle: null, // 付款周期
|
||||
taxRate: null, // 税率
|
||||
chargeType: 1, //计费模式
|
||||
priceUnit: 1, //合同单价默认单位
|
||||
dayPriceAlgorithm: 1, //每天单价算法
|
||||
dayPrice: null, //每天单价
|
||||
dayPriceUnit: 1, //每天单价单位
|
||||
calculationAccuracy: 1, //计算精度
|
||||
billAccuracy: 1, //账单小数点
|
||||
partitionMode: 1, //租期划分模式
|
||||
firstLessMonthType: 1, //首月不足月租金处理
|
||||
otherLeaseProcessing: 1, //免租期、装修期租期处理
|
||||
};
|
||||
|
||||
const form = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
payCycle: [{ required: true, message: '请输入付款周期' }],
|
||||
taxRate: [{ required: true, message: '请输入税率' }],
|
||||
chargeType: [{ required: true, message: '请选择计费模式' }],
|
||||
priceUnit: [{ required: true, message: '请选择合同单价默认单位' }],
|
||||
dayPriceAlgorithm: [{ required: true, message: '请选择每天单价算法' }],
|
||||
dayPrice: [{ required: true, message: '请输入每天单价' }],
|
||||
});
|
||||
|
||||
const handleOk = async () => {
|
||||
if (isLoading.value) return;
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const valid = await formRef.value?.validate();
|
||||
if (valid) return;
|
||||
emit('update:visible', false);
|
||||
emit('submitted', form);
|
||||
} catch {
|
||||
console.warn('❌ 校验未通过');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
const dayPriceCalc = computed(() => {
|
||||
const unit = priceUnitMap.get(form.dayPriceUnit);
|
||||
return `${form.dayPrice}${unit}`;
|
||||
});
|
||||
|
||||
// 打开抽屉时,用 record 合并默认值填充表单;编辑场景回填
|
||||
function applyRecordToForm(record: any) {
|
||||
const merged = { ...defaultForm, ...(record || {}) };
|
||||
Object.keys(defaultForm).forEach((key) => {
|
||||
// 仅覆盖已定义字段,保持响应式
|
||||
(form as any)[key] = (merged as any)[key];
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
applyRecordToForm(props.record);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.record,
|
||||
(val) => {
|
||||
if (props.visible) {
|
||||
applyRecordToForm(val);
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(label.arco-form-item-label) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑创建合同模板' : '新增创建合同模板'"
|
||||
title="新增创建合同模板"
|
||||
unmount-on-close
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
import { createTemplateSetting, updateTemplateSetting } from '@/api/setting';
|
||||
import { DepartmentItem, CreateTemplateSettingParam } from '@/types/setting';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
interface TreeNode {
|
||||
id: string | number;
|
||||
name: string;
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
isEdit: boolean;
|
||||
record?: DepartmentItem | null;
|
||||
record?: any;
|
||||
departmentListData: TreeNode[]; // 修复类型
|
||||
}>();
|
||||
|
||||
|
|
@ -54,13 +54,13 @@
|
|||
(e: 'update:visible', val: boolean): void;
|
||||
(e: 'submitted'): void;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const form = reactive<CreateTemplateSettingParam>({
|
||||
const form = reactive<any>({
|
||||
name: '',
|
||||
departmentId: null,
|
||||
// id: undefined,
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
// 校验规则
|
||||
|
|
@ -68,19 +68,18 @@
|
|||
name: [{ required: true, message: '请输入模板名称' }],
|
||||
departmentId: [{ required: true, message: '请选择适用范围' }],
|
||||
};
|
||||
|
||||
// Watch 弹窗控制 & 初始化
|
||||
// 表单事件
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (props.record) {
|
||||
// form.id = props.record?.id;
|
||||
// form.name = props.record?.name;
|
||||
// form.parent_id = props.record?.parentId ?? '';
|
||||
// form.user_id = props.record?.UserId ?? '';
|
||||
// form.remark = props.record?.remark ?? '';
|
||||
// Object.assign(form, props.record);
|
||||
Object.assign(form, {
|
||||
...props.record,
|
||||
});
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
|
|
@ -92,21 +91,29 @@
|
|||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 表单事件
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate();
|
||||
const { code, message } = props.isEdit
|
||||
if (!form.departmentId || !form.name) return;
|
||||
if (props.isEdit) {
|
||||
// saveType 保存类别:1 名称与适用范围数据,2 基础信息数据,3 条款数据)
|
||||
form.saveType = 1;
|
||||
}
|
||||
const { code, message, data } = props.isEdit
|
||||
? await updateTemplateSetting(form)
|
||||
: await createTemplateSetting(form);
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
if (props.isEdit) {
|
||||
emit('submitted');
|
||||
} else {
|
||||
const info = data as any;
|
||||
const id = info.contractTemplateSettingId;
|
||||
router.push({
|
||||
name: 'system-setting-contract-template-edit',
|
||||
query: { id },
|
||||
});
|
||||
}
|
||||
handleCancel();
|
||||
// 通知父组件更新部门列表数据
|
||||
emit('submitted');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
113
src/views/setting/system/contract-settings/terms/bond.vue
Normal file
113
src/views/setting/system/contract-settings/terms/bond.vue
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px]">
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">{{ title }}</h4>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="bondForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="保证金金额" field="num" required>
|
||||
<a-input-number
|
||||
v-model="bondForm.num"
|
||||
:min="1"
|
||||
placeholder="请输入数字"
|
||||
/>
|
||||
<ul class="options ml-[10px]">
|
||||
<li
|
||||
v-for="item in bondUnits"
|
||||
:key="item.type"
|
||||
:class="[
|
||||
'subItem',
|
||||
{ isActive: item.type === bondForm.priceType },
|
||||
]"
|
||||
@click="bondForm.priceType = item.type"
|
||||
>
|
||||
{{ item.text }}</li
|
||||
>
|
||||
</ul>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="付款日期" field="payType" required>
|
||||
<ul class="options">
|
||||
<li
|
||||
v-for="item in payDateTypes"
|
||||
:key="item.type"
|
||||
:class="[
|
||||
'subItem',
|
||||
{ isActive: item.type === bondForm.payType },
|
||||
]"
|
||||
@click="choosePayDateType(item.type)"
|
||||
>
|
||||
{{ item.text }}</li
|
||||
>
|
||||
</ul>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
defineProps<{ title?: string }>();
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
num: null,
|
||||
payType: 1,
|
||||
priceType: 1,
|
||||
};
|
||||
const bondForm = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
num: [{ required: true, message: '请输入保证金金额' }],
|
||||
payType: [{ required: true, message: '请选择保证金单位' }],
|
||||
priceType: [{ required: true, message: '请选择付款日期' }],
|
||||
});
|
||||
const payDateTypes = [
|
||||
{ type: 1, text: '首期账单日期' },
|
||||
{ type: 2, text: '签约日期' },
|
||||
];
|
||||
const bondUnits = [
|
||||
{ type: 1, text: '个月' },
|
||||
{ type: 2, text: '元' },
|
||||
];
|
||||
const choosePayDateType = (type) => {
|
||||
bondForm.payType = type;
|
||||
};
|
||||
|
||||
// 暴露保存数据
|
||||
function getData() {
|
||||
return { ...bondForm };
|
||||
}
|
||||
defineExpose({ getData });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
127
src/views/setting/system/contract-settings/terms/discount.vue
Normal file
127
src/views/setting/system/contract-settings/terms/discount.vue
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<div
|
||||
class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px] mt-[20px]"
|
||||
>
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">优惠条款</h4>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="discountForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="7">
|
||||
<a-form-item label="优惠类型" field="type" required>
|
||||
<a-select v-model="discountForm.type" placeholder="请选择">
|
||||
<a-option :value="1">单价折扣</a-option>
|
||||
<a-option :value="2">单价减免</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="金额折扣" field="discount" required>
|
||||
<a-input
|
||||
v-model="discountForm.discount"
|
||||
append="折"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="11">
|
||||
<a-form-item label="作用范围" required>
|
||||
<a-input-group>
|
||||
<a-form-item no-style field="startNum">
|
||||
<a-input
|
||||
v-model="discountForm.startNum"
|
||||
prepend="第"
|
||||
append="期开始"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item no-style field="endNum">
|
||||
<a-input
|
||||
v-model="discountForm.endNum"
|
||||
prepend="持续"
|
||||
append="期"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
type: 1,
|
||||
discount: null,
|
||||
startNum: null,
|
||||
endNum: null,
|
||||
};
|
||||
const discountForm = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
type: [{ required: true, message: '优惠类型不能为空' }],
|
||||
discount: [{ required: true, message: '金额折扣不能为空' }],
|
||||
startNum: [{ required: true, message: '开始期数不能为空' }],
|
||||
endNum: [{ required: true, message: '持续期数不能为空' }],
|
||||
});
|
||||
|
||||
// 向父组件暴露数据与校验方法
|
||||
function getData() {
|
||||
const isEmptyValue = (v: any) =>
|
||||
v === undefined ||
|
||||
v === null ||
|
||||
(typeof v === 'string' && v.trim() === '');
|
||||
const { type, discount, startNum, endNum } = discountForm;
|
||||
// 任一必填未有效填写则返回空对象,避免父层误判有数据
|
||||
if (
|
||||
isEmptyValue(type) ||
|
||||
isEmptyValue(discount) ||
|
||||
isEmptyValue(startNum) ||
|
||||
isEmptyValue(endNum)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
type: Number(type),
|
||||
discount: Number(discount),
|
||||
startNum: Number(startNum),
|
||||
endNum: Number(endNum),
|
||||
};
|
||||
}
|
||||
async function validate() {
|
||||
return formRef.value?.validate?.();
|
||||
}
|
||||
defineExpose({ getData, validate });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
134
src/views/setting/system/contract-settings/terms/incremental.vue
Normal file
134
src/views/setting/system/contract-settings/terms/incremental.vue
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<template>
|
||||
<div
|
||||
class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px] mt-[20px]"
|
||||
>
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">递增条款</h4>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="increamForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="递增间隔" field="num" required>
|
||||
<a-input-number
|
||||
v-model="increamForm.num"
|
||||
:min="1"
|
||||
prepend="每隔"
|
||||
append="年"
|
||||
placeholder="请输入数字"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="单价递增" field="priceIncrease" required>
|
||||
<a-input v-model="increamForm.priceIncrease" placeholder="请选择">
|
||||
<template #append>
|
||||
<a-select
|
||||
v-model="increamForm.priceIncreaseType"
|
||||
style="width: 80px"
|
||||
>
|
||||
<a-option :value="1">%</a-option>
|
||||
<a-option :value="2">元</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="保证金递增" field="bondIncrease" required>
|
||||
<a-input v-model="increamForm.bondIncrease" placeholder="请选择">
|
||||
<template #append>
|
||||
<a-select
|
||||
v-model="increamForm.bondIncreaseType"
|
||||
style="width: 80px"
|
||||
>
|
||||
<a-option :value="1">%</a-option>
|
||||
<a-option :value="2">元</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
num: null,
|
||||
priceIncrease: null,
|
||||
priceIncreaseType: 1,
|
||||
bondIncrease: null,
|
||||
bondIncreaseType: 1,
|
||||
};
|
||||
const increamForm = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
num: [{ required: true, message: '递增间隔不能为空' }],
|
||||
priceIncrease: [{ required: true, message: '单价递增不能为空' }],
|
||||
bondIncrease: [{ required: true, message: '保证金递增不能为空' }],
|
||||
});
|
||||
|
||||
// 向父组件暴露数据与校验方法
|
||||
function getData() {
|
||||
const isEmptyValue = (v: any) =>
|
||||
v === undefined ||
|
||||
v === null ||
|
||||
(typeof v === 'string' && v.trim() === '');
|
||||
const {
|
||||
num,
|
||||
priceIncrease,
|
||||
priceIncreaseType,
|
||||
bondIncrease,
|
||||
bondIncreaseType,
|
||||
} = increamForm;
|
||||
if (
|
||||
isEmptyValue(num) ||
|
||||
isEmptyValue(priceIncrease) ||
|
||||
isEmptyValue(bondIncrease)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
num: Number(num),
|
||||
priceIncrease: Number(priceIncrease),
|
||||
priceIncreaseType: Number(priceIncreaseType),
|
||||
bondIncrease: Number(bondIncrease),
|
||||
bondIncreaseType: Number(bondIncreaseType),
|
||||
};
|
||||
}
|
||||
async function validate() {
|
||||
return formRef.value?.validate?.();
|
||||
}
|
||||
defineExpose({ getData, validate });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
144
src/views/setting/system/contract-settings/terms/late-fee.vue
Normal file
144
src/views/setting/system/contract-settings/terms/late-fee.vue
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<template>
|
||||
<div
|
||||
class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px] mt-[20px]"
|
||||
>
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">滞纳金条款</h4>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="lateFeeForm"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="7">
|
||||
<a-form-item label="起算天数" field="days" required>
|
||||
<a-input-number
|
||||
v-model="lateFeeForm.days"
|
||||
:min="1"
|
||||
prepend="账单逾期"
|
||||
append="天"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="收取方式" field="chargeType" required>
|
||||
<a-select v-model="lateFeeForm.chargeType" placeholder="请选择">
|
||||
<a-option :value="1">仅计算不生成账单</a-option>
|
||||
<a-option :value="2">达到上限生成账单</a-option>
|
||||
<a-option :value="3">生成账单每天调</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="11">
|
||||
<a-form-item label="滞纳金" field="ratio" required>
|
||||
<a-input-group>
|
||||
<a-input v-model="lateFeeForm.ratio" placeholder="请输入">
|
||||
<template #append>
|
||||
<a-select
|
||||
v-model="lateFeeForm.ratioType"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-option :value="1">%/天</a-option>
|
||||
<a-option :value="2">元/天</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input>
|
||||
<a-input v-model="lateFeeForm.ceiling" placeholder="请输入">
|
||||
<template #append>
|
||||
<a-select
|
||||
v-model="lateFeeForm.ceilingType"
|
||||
style="width: 100px"
|
||||
>
|
||||
<a-option :value="1">%上限</a-option>
|
||||
<a-option :value="2">元上限</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
// 表单数据
|
||||
const formRef = ref<FormInstance>();
|
||||
const defaultForm: any = {
|
||||
days: null,
|
||||
chargeType: 1,
|
||||
ratio: null,
|
||||
ratioType: 1,
|
||||
ceiling: null,
|
||||
ceilingType: 1,
|
||||
};
|
||||
const lateFeeForm = reactive<any>({ ...defaultForm });
|
||||
// 校验规则
|
||||
const rules = reactive({
|
||||
days: [{ required: true, message: '起算天数不能为空' }],
|
||||
chargeType: [{ required: true, message: '收取方式不能为空' }],
|
||||
ratio: [{ required: true, message: '滞纳金不能为空' }],
|
||||
ratioType: [{ required: true, message: '滞纳金单位不能为空' }],
|
||||
ceiling: [{ required: true, message: '上限不能为空' }],
|
||||
ceilingType: [{ required: true, message: '上限单位不能为空' }],
|
||||
});
|
||||
|
||||
// 向父组件暴露数据与校验方法
|
||||
function getData() {
|
||||
const isEmptyValue = (v: any) =>
|
||||
v === undefined ||
|
||||
v === null ||
|
||||
(typeof v === 'string' && v.trim() === '');
|
||||
const { days, chargeType, ratio, ratioType, ceiling, ceilingType } =
|
||||
lateFeeForm;
|
||||
if (
|
||||
isEmptyValue(days) ||
|
||||
isEmptyValue(chargeType) ||
|
||||
isEmptyValue(ratio) ||
|
||||
isEmptyValue(ratioType) ||
|
||||
isEmptyValue(ceiling) ||
|
||||
isEmptyValue(ceilingType)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
days: Number(days),
|
||||
chargeType: Number(chargeType),
|
||||
ratio: Number(ratio),
|
||||
ratioType: Number(ratioType),
|
||||
ceiling: Number(ceiling),
|
||||
ceilingType: Number(ceilingType),
|
||||
};
|
||||
}
|
||||
async function validate() {
|
||||
return formRef.value?.validate?.();
|
||||
}
|
||||
defineExpose({ getData, validate });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
<template>
|
||||
<div
|
||||
class="border-solid border-[1px] border-[#f0f0f0] px-[10px] py-[15px] mt-[20px]"
|
||||
>
|
||||
<h4 class="text-[14px] text-[#505968] font-extrabold">租期条款(租金)</h4>
|
||||
<div class="bg-[#edfeec] px-[10px] py-[5px]">
|
||||
<ul v-if="hasRentSetting" class="rent-list">
|
||||
<li v-for="(text, idx) in displayItems" :key="idx" class="rent-item">{{
|
||||
text
|
||||
}}</li>
|
||||
<li class="rent-action" @click="handleShowDrawer">
|
||||
<icon-settings :size="18" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="mt-[15px]"
|
||||
:model="rentSettingInfo"
|
||||
:rules="rules"
|
||||
auto-label-width
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="首期账单缴费日期" field="firstPayDate" required>
|
||||
<a-date-picker
|
||||
v-model="rentSettingInfo.firstPayDate"
|
||||
:disabled-date="(current) => dayjs(current).isBefore(dayjs())"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="账单缴费日期" field="payDay" required>
|
||||
<a-input-number v-model="rentSettingInfo.payDay">
|
||||
<template #prepend>
|
||||
<a-select v-model="rentSettingInfo.payDateType">
|
||||
<a-option :value="1">租期开始前</a-option>
|
||||
<a-option :value="2">租期开始后</a-option>
|
||||
<a-option :value="3">租期开始上月</a-option>
|
||||
<a-option :value="4">租期开始本月</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<template #append>
|
||||
<a-select v-model="rentSettingInfo.payDayType">
|
||||
<a-option :value="1">日付款</a-option>
|
||||
<a-option :value="2">自然日付款</a-option>
|
||||
<a-option :value="3">工作日付款</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="默认收款账户"
|
||||
field="financeTurnoverAccountSettingId"
|
||||
required
|
||||
>
|
||||
<a-select v-model="rentSettingInfo.financeTurnoverAccountSettingId">
|
||||
<a-option
|
||||
v-for="item in turnoverAccountList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.bankAccuntName }}</a-option
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
<rentSettingDrawer
|
||||
v-model:visible="isShowDrawer"
|
||||
:record="rentSettingInfo"
|
||||
@submitted="getRendSetting"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue';
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
import rentSettingDrawer from '../drawer/rent-setting-drawer.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { getUseTurnoverAccountList } from '@/api/setting';
|
||||
const isShowDrawer = ref<boolean>(false);
|
||||
// 与抽屉保持一致的默认表单,用于初始回显与编辑回填
|
||||
const defaultForm: any = {
|
||||
payCycle: null, // 付款周期 (输入类,默认不显示)
|
||||
taxRate: null, // 税率 (输入类,默认不显示)
|
||||
chargeType: 1, // 计费模式
|
||||
priceUnit: 1, // 合同单价默认单位
|
||||
dayPriceAlgorithm: 1, // 每天单价算法
|
||||
dayPrice: null, // 每天单价 (输入类,默认不显示)
|
||||
dayPriceUnit: 1, // 每天单价单位
|
||||
calculationAccuracy: 1, // 计算精度
|
||||
billAccuracy: 1, // 账单小数点
|
||||
partitionMode: 1, // 租期划分模式
|
||||
firstLessMonthType: 1, // 首月不足月租金处理
|
||||
otherLeaseProcessing: 1, // 免租期、装修期租期处理
|
||||
firstPayDate: '',
|
||||
payDay: '',
|
||||
payDateType: 1,
|
||||
payDayType: 1,
|
||||
};
|
||||
const rentSettingInfo = ref<any>({ ...defaultForm });
|
||||
const formRef = ref<FormInstance>();
|
||||
const rules = reactive({
|
||||
firstPayDate: [{ required: true, message: '请选择首期账单缴费日期' }],
|
||||
payDay: [{ required: true, message: '请输入账单缴费日期' }],
|
||||
financeTurnoverAccountSettingId: [
|
||||
{ required: true, message: '请选择默认收款账户' },
|
||||
],
|
||||
});
|
||||
|
||||
const turnoverAccountList = ref<any>([]);
|
||||
const props = defineProps<{
|
||||
termSettingInfo?: any | null;
|
||||
}>();
|
||||
// 文案映射与计算属性
|
||||
const chargeTypeMap = new Map([
|
||||
[1, '按月计算租金'],
|
||||
[2, '按实际天数计算租金'],
|
||||
]);
|
||||
const priceUnitMap = new Map([
|
||||
[1, '元/㎡*月'],
|
||||
[2, '元/㎡*天'],
|
||||
[3, '元/天'],
|
||||
[4, '元/月'],
|
||||
[5, '元/年'],
|
||||
]);
|
||||
const dayPriceAlgorithmMap = new Map([
|
||||
[1, '按当前年天数换算'],
|
||||
[2, '按当前月天数换算'],
|
||||
[3, '按自定义每天单价'],
|
||||
]);
|
||||
const calculationAccuracyMap = new Map([
|
||||
[1, '每步计算进行精确计算,直至计算结果。'],
|
||||
[2, '每步计算保留2位小数,直至计算结果'],
|
||||
[3, '每步计算保留1位小数,直至计算结果'],
|
||||
[4, '每步计算不保留小数,直至计算结果'],
|
||||
]);
|
||||
const billAccuracyMap = new Map([
|
||||
[1, '2位小数'],
|
||||
[2, '1位小数'],
|
||||
[3, '不保留'],
|
||||
]);
|
||||
const partitionModeMap = new Map([
|
||||
[1, '按自然月划分租期(“周期开始月”月首日到“后期结束月”月末日)'],
|
||||
[2, '按起止日划分租期'],
|
||||
]);
|
||||
const firstLessMonthTypeMap = new Map([
|
||||
[1, '首月不足月租期:独立为一个租期'],
|
||||
[2, '首月不足月租期:作为完整月来划分租期'],
|
||||
[3, '首月不足月租期:与下一租期合并为一个租期'],
|
||||
]);
|
||||
const otherLeaseProcessingMap = new Map([
|
||||
[1, '免租期计入租期,装修期计入租期'],
|
||||
[2, '免租期计入租期,装修期不计入租期'],
|
||||
[3, '免租期不计入租期,装修期计入租期'],
|
||||
[4, '免租期不计入租期,装修期不计入租期'],
|
||||
]);
|
||||
|
||||
const hasRentSetting = computed(
|
||||
() => Object.keys(rentSettingInfo.value || {}).length > 0
|
||||
);
|
||||
// 汇总所有字段的回显文案
|
||||
const displayItems = computed(() => {
|
||||
const list: string[] = [];
|
||||
const v = rentSettingInfo.value || {};
|
||||
// 计费相关
|
||||
if (v.payCycle !== undefined && v.payCycle !== null)
|
||||
list.push(`${v.payCycle}月1付`);
|
||||
if (v.taxRate !== undefined && v.taxRate !== null && v.taxRate !== '')
|
||||
list.push(`税率${v.taxRate}%`);
|
||||
if (v.chargeType !== undefined)
|
||||
list.push(chargeTypeMap.get(v.chargeType) || '');
|
||||
if (v.priceUnit !== undefined)
|
||||
list.push(`合同默认单价${priceUnitMap.get(v.priceUnit) || ''}`);
|
||||
if (v.dayPriceAlgorithm !== undefined)
|
||||
list.push(
|
||||
`每天单价算法:${dayPriceAlgorithmMap.get(v.dayPriceAlgorithm) || ''}`
|
||||
);
|
||||
if (v.dayPrice !== undefined && v.dayPrice !== null && v.dayPrice !== '') {
|
||||
const unit = priceUnitMap.get(v.dayPriceUnit);
|
||||
list.push(`每天单价${v.dayPrice}${unit ? unit : ''}`);
|
||||
}
|
||||
if (v.calculationAccuracy !== undefined)
|
||||
list.push(
|
||||
`计算精度:${calculationAccuracyMap.get(v.calculationAccuracy) || ''}`
|
||||
);
|
||||
if (v.billAccuracy !== undefined)
|
||||
list.push(`每期账单小数点:${billAccuracyMap.get(v.billAccuracy) || ''}`);
|
||||
// 租期相关
|
||||
if (v.partitionMode !== undefined)
|
||||
list.push(`租期划分模式:${partitionModeMap.get(v.partitionMode) || ''}`);
|
||||
if (v.firstLessMonthType !== undefined)
|
||||
list.push(firstLessMonthTypeMap.get(v.firstLessMonthType) || '');
|
||||
if (v.otherLeaseProcessing !== undefined)
|
||||
list.push(
|
||||
`免租/装修期处理:${
|
||||
otherLeaseProcessingMap.get(v.otherLeaseProcessing) || ''
|
||||
}`
|
||||
);
|
||||
// 过滤空串
|
||||
return list.filter(Boolean);
|
||||
});
|
||||
function handleShowDrawer() {
|
||||
isShowDrawer.value = true;
|
||||
}
|
||||
// 暴露方法给父组件用于保存/校验
|
||||
function getData() {
|
||||
return { ...rentSettingInfo.value };
|
||||
}
|
||||
async function validate() {
|
||||
const res = await formRef.value?.validate();
|
||||
if (res) throw new Error('租金设置校验未通过');
|
||||
return true;
|
||||
}
|
||||
function getRendSetting(value) {
|
||||
rentSettingInfo.value = { ...rentSettingInfo.value, ...(value || {}) };
|
||||
}
|
||||
async function getUseTurnoverAccountListData(info) {
|
||||
const params = {
|
||||
fee_attribute: 1,
|
||||
fee_type: info?.type || 1,
|
||||
is_bond: info?.isBond || 1,
|
||||
};
|
||||
const { code, data } = await getUseTurnoverAccountList(params);
|
||||
if (code === 200) {
|
||||
turnoverAccountList.value = data?.list || [];
|
||||
}
|
||||
}
|
||||
// 避免重复触发:使用 key 去重与并发锁
|
||||
const lastTermKey = ref<string>('');
|
||||
const isFetching = ref<boolean>(false);
|
||||
function buildTermKey(info: any) {
|
||||
if (!info) return '';
|
||||
return `${info?.id || ''}-${info?.type || ''}-${info?.isBond || ''}`;
|
||||
}
|
||||
async function triggerTurnoverList(info: any) {
|
||||
const key = buildTermKey(info);
|
||||
if (!key || key === lastTermKey.value || isFetching.value) return;
|
||||
lastTermKey.value = key;
|
||||
isFetching.value = true;
|
||||
try {
|
||||
await getUseTurnoverAccountListData(info);
|
||||
} finally {
|
||||
isFetching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
triggerTurnoverList(props.termSettingInfo);
|
||||
});
|
||||
watch(
|
||||
() => props.termSettingInfo,
|
||||
(newVal) => {
|
||||
triggerTurnoverList(newVal);
|
||||
},
|
||||
{ deep: false }
|
||||
);
|
||||
|
||||
defineExpose({ getData, validate });
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f2f3f5;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 3px;
|
||||
|
||||
.subItem {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.isActive {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rent-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
|
||||
.rent-item {
|
||||
line-height: 22px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(232, 255, 234, 1);
|
||||
color: rgba(0, 180, 42, 1);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(0, 180, 42, 1);
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.rent-action {
|
||||
margin-left: auto;
|
||||
order: 9999;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
<template>
|
||||
<a-card title="合同管理">
|
||||
<a-spin style="display: block" :loading="loading">
|
||||
<div
|
||||
v-if="contractTagsList && contractTagsList.length"
|
||||
class="project_label_wrap"
|
||||
>
|
||||
<div class="flex_center_between">
|
||||
<div class="title_wrap">
|
||||
<h4>合同类型(标签类)</h4>
|
||||
<a-checkbox
|
||||
:model-value="
|
||||
checkedLabelId.length === contractTagsList.length &&
|
||||
contractTagsList.length > 0
|
||||
"
|
||||
@change="onCheckAllLabelChange"
|
||||
>全选</a-checkbox
|
||||
>
|
||||
</div>
|
||||
<a-button type="text" @click="handleAddLabel"
|
||||
><icon-plus />新增</a-button
|
||||
>
|
||||
<labelFormModal
|
||||
v-model:visible="showLabelModal"
|
||||
:create-api="createContractTags"
|
||||
:update-api="updateContractTags"
|
||||
:del-api="delContractTags"
|
||||
:title="isEdit ? '编辑合同类型字典' : '新增合同类型字典'"
|
||||
:record="currentLabelFormInfo"
|
||||
:is-edit="isEdit"
|
||||
@submitted="getAllContractTagListData"
|
||||
/>
|
||||
</div>
|
||||
<ul class="ul_wrap">
|
||||
<li
|
||||
v-for="item in contractTagsList"
|
||||
:key="item.id"
|
||||
style="margin: 5px 30px 0 0"
|
||||
>
|
||||
<a-checkbox
|
||||
:model-value="checkedLabelId.includes(item.id)"
|
||||
@change="(val) => onProjectTypeCheckChange(item, val)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<span
|
||||
class="circle"
|
||||
:style="{
|
||||
borderColor: item.color,
|
||||
backgroundColor: item.color,
|
||||
}"
|
||||
></span>
|
||||
<icon-edit
|
||||
v-if="item.type === 2"
|
||||
class="edit_icon"
|
||||
@click.stop.prevent="handleEditLabel(item, $event)"
|
||||
/>
|
||||
</a-checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import {
|
||||
getAllContractTagsList,
|
||||
checkContractTags,
|
||||
checkAllContractTags,
|
||||
createContractTags,
|
||||
updateContractTags,
|
||||
delContractTags,
|
||||
} from '@/api/setting';
|
||||
import { ProjectTypeChecked } from '@/types/comm';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import labelFormModal from './label-form-modal.vue';
|
||||
|
||||
const contractTagsList = ref<any>([]);
|
||||
const checkedLabelId = ref<number[]>([]);
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
const showLabelModal = ref<boolean>(false);
|
||||
const isEdit = ref<boolean>(false);
|
||||
const currentLabelFormInfo = ref(null);
|
||||
|
||||
const checkAllProjectTypeData = async (checked: boolean) => {
|
||||
try {
|
||||
const params = { is_check: (checked ? 1 : 0) as ProjectTypeChecked };
|
||||
const { code, message } = await checkAllContractTags(params);
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
}
|
||||
};
|
||||
const checkProjectTypeData = async (params: {
|
||||
id: number;
|
||||
is_check: ProjectTypeChecked;
|
||||
}) => {
|
||||
try {
|
||||
const { code, message } = await checkContractTags(params);
|
||||
if (code === 200) {
|
||||
Message.success(message);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
}
|
||||
};
|
||||
const onCheckAllLabelChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
// 全选
|
||||
checkedLabelId.value = contractTagsList.value.map((item) => item.id);
|
||||
} else {
|
||||
// 取消全选
|
||||
checkedLabelId.value = [];
|
||||
}
|
||||
checkAllProjectTypeData(checked);
|
||||
};
|
||||
const onProjectTypeCheckChange = (item, checked) => {
|
||||
if (checked) {
|
||||
if (!checkedLabelId.value.includes(item.id)) {
|
||||
checkedLabelId.value.push(item.id);
|
||||
}
|
||||
} else {
|
||||
checkedLabelId.value = checkedLabelId.value.filter(
|
||||
(id) => id !== item.id
|
||||
);
|
||||
}
|
||||
checkProjectTypeData({ id: item.id, is_check: checked ? 1 : 0 });
|
||||
};
|
||||
const getAllContractTagListData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { code, data } = await getAllContractTagsList();
|
||||
if (code === 200 && data) {
|
||||
contractTagsList.value = data?.contractTagsSettingList;
|
||||
if (contractTagsList.value?.length === 0) return;
|
||||
checkedLabelId.value = contractTagsList.value
|
||||
.filter((i) => i.isCheck === 1)
|
||||
.map((i) => i.id);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const handleAddLabel = () => {
|
||||
showLabelModal.value = true;
|
||||
isEdit.value = false;
|
||||
currentLabelFormInfo.value = null;
|
||||
};
|
||||
const handleEditLabel = (item, event) => {
|
||||
if (event) event.preventDefault();
|
||||
showLabelModal.value = true;
|
||||
isEdit.value = true;
|
||||
currentLabelFormInfo.value = item;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getAllContractTagListData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.project_label_wrap {
|
||||
.title_wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.circle {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 50%;
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.edit_icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.ul_wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
<a-col :span="18">
|
||||
<projectCard v-if="currentSettingId === 2" />
|
||||
<roomCard v-if="currentSettingId === 3" />
|
||||
<contractCard v-if="currentSettingId === 4" />
|
||||
<tenantCard v-if="currentSettingId === 5" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
import { dataDictionaryTabs } from './contrant';
|
||||
import projectCard from './components/project-card.vue';
|
||||
import roomCard from './components/room-card.vue';
|
||||
import contractCard from './components/contract-card.vue';
|
||||
import tenantCard from './components/tenant-card.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
|
|||
34
tailwind.config.ts
Normal file
34
tailwind.config.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./index.html',
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx}', // 扫描所有 Vue/TS/JS 文件,保证类名能被提取
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// 让 Tailwind 能直接用 Arco Design 的主题变量
|
||||
primary: 'var(--color-primary)',
|
||||
success: 'var(--color-success)',
|
||||
warning: 'var(--color-warning)',
|
||||
danger: 'var(--color-danger)',
|
||||
},
|
||||
borderRadius: {
|
||||
// 覆盖 Tailwind 的圆角,用 Arco token
|
||||
lg: 'var(--border-radius-medium)',
|
||||
xl: 'var(--border-radius-large)',
|
||||
},
|
||||
fontSize: {
|
||||
// 对齐 Arco Design 的字体大小
|
||||
sm: 'var(--font-size-small)',
|
||||
base: 'var(--font-size-body-3)',
|
||||
lg: 'var(--font-size-title-2)',
|
||||
},
|
||||
},
|
||||
},
|
||||
darkMode: 'class', // 跟 Arco 的暗黑模式配合
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
Loading…
Add table
Reference in a new issue