mirror of
http://183.237.82.114:10086/kilo/leaser_front.git
synced 2026-10-24 22:40:51 +09: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
|
/*.json
|
||||||
/*.js
|
/*.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');
|
const path = require('path');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
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: {
|
env: {
|
||||||
'browser': true,
|
'browser': true,
|
||||||
'node': true,
|
'node': true,
|
||||||
|
'es2021': true,
|
||||||
'vue/setup-compiler-macros': 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: [
|
extends: [
|
||||||
// Airbnb JavaScript Style Guide https://github.com/airbnb/javascript
|
'plugin:vue/vue3-recommended',
|
||||||
'airbnb-base',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'plugin:import/recommended',
|
'plugin:import/recommended',
|
||||||
'plugin:import/typescript',
|
'plugin:import/typescript',
|
||||||
'plugin:vue/vue3-recommended',
|
|
||||||
'plugin:prettier/recommended',
|
'plugin:prettier/recommended',
|
||||||
],
|
],
|
||||||
settings: {
|
plugins: ['vue', '@typescript-eslint', 'import', 'prettier'],
|
||||||
'import/resolver': {
|
|
||||||
typescript: {
|
|
||||||
project: path.resolve(__dirname, './tsconfig.json'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
|
// 你的规则保持不变
|
||||||
|
'no-param-reassign': 0,
|
||||||
|
'no-plusplus': 0,
|
||||||
'prettier/prettier': 1,
|
'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/require-default-prop': 0,
|
||||||
'vue/singleline-html-element-content-newline': 0,
|
'vue/singleline-html-element-content-newline': 0,
|
||||||
'vue/max-attributes-per-line': 0,
|
'vue/max-attributes-per-line': 0,
|
||||||
// Vue: Add extra rules
|
|
||||||
'vue/custom-event-name-casing': [0, 'camelCase'],
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'camelcase': ['warn', { properties: 'never' }],
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'vue/no-v-text': 1,
|
'warn',
|
||||||
'vue/padding-line-between-blocks': 1,
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||||
'vue/require-direct-export': 1,
|
],
|
||||||
'vue/multi-word-component-names': 0,
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
// Allow @ts-ignore comment
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
'@typescript-eslint/ban-ts-comment': 0,
|
'@typescript-eslint/ban-types': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 1,
|
|
||||||
'@typescript-eslint/no-empty-function': 1,
|
'import/no-unresolved': [2, { ignore: ['^@/'] }],
|
||||||
'@typescript-eslint/no-explicit-any': 0, // 改为警告
|
|
||||||
'no-unused-expressions': 'off',
|
|
||||||
'no-console': process.env.NODE_ENV === 'production' ? 2 : 1, // 生产环境禁用console
|
|
||||||
'import/extensions': [
|
'import/extensions': [
|
||||||
2,
|
'error',
|
||||||
'ignorePackages',
|
'ignorePackages',
|
||||||
{
|
{
|
||||||
js: 'never',
|
js: 'never',
|
||||||
jsx: 'never',
|
|
||||||
ts: 'never',
|
ts: 'never',
|
||||||
tsx: '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",
|
"prettier --write",
|
||||||
"eslint --fix"
|
"eslint --fix"
|
||||||
],
|
],
|
||||||
|
"*.d.ts": [
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
"*.{less,css}": [
|
"*.{less,css}": [
|
||||||
"stylelint --fix",
|
"stylelint --fix",
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
|
|
@ -31,6 +34,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arco-design/web-vue": "^2.57.0",
|
"@arco-design/web-vue": "^2.57.0",
|
||||||
|
"@onlyoffice/document-editor-vue": "^1.6.1",
|
||||||
"@vueuse/core": "^9.3.0",
|
"@vueuse/core": "^9.3.0",
|
||||||
"arco-design-pro-vue": "^2.7.3",
|
"arco-design-pro-vue": "^2.7.3",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
|
|
@ -39,7 +43,7 @@
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mitt": "^3.0.0",
|
"mitt": "^3.0.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.0.23",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.3.0",
|
"pinia-plugin-persistedstate": "^4.3.0",
|
||||||
"query-string": "^8.0.3",
|
"query-string": "^8.0.3",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
|
|
@ -53,38 +57,42 @@
|
||||||
"@arco-plugins/vite-vue": "^1.4.5",
|
"@arco-plugins/vite-vue": "^1.4.5",
|
||||||
"@commitlint/cli": "^17.1.2",
|
"@commitlint/cli": "^17.1.2",
|
||||||
"@commitlint/config-conventional": "^17.1.0",
|
"@commitlint/config-conventional": "^17.1.0",
|
||||||
|
"@tailwindcss/postcss": "^4.1.13",
|
||||||
"@types/lodash": "^4.14.186",
|
"@types/lodash": "^4.14.186",
|
||||||
"@types/mockjs": "^1.0.7",
|
"@types/mockjs": "^1.0.7",
|
||||||
"@types/node": "^24.0.3",
|
"@types/node": "^24.0.3",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/sortablejs": "^1.15.0",
|
"@types/sortablejs": "^1.15.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||||
"@typescript-eslint/parser": "^5.40.0",
|
"@typescript-eslint/parser": "^8.46.0",
|
||||||
"@vitejs/plugin-vue": "^3.1.2",
|
"@vitejs/plugin-vue": "^3.1.2",
|
||||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||||
"@vue/babel-plugin-jsx": "^1.1.1",
|
"@vue/babel-plugin-jsx": "^1.1.1",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
"consola": "^2.15.3",
|
"consola": "^2.15.3",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.25.0",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.10.2",
|
||||||
"eslint-import-resolver-typescript": "^3.5.1",
|
"eslint-import-resolver-typescript": "^3.10.1",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.5",
|
||||||
"eslint-plugin-vue": "^9.6.0",
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
|
"globals": "^16.4.0",
|
||||||
"husky": "^8.0.1",
|
"husky": "^8.0.1",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"postcss-html": "^1.8.0",
|
"postcss-html": "^1.8.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.8",
|
||||||
"rollup": "^3.9.1",
|
"rollup": "^3.9.1",
|
||||||
"rollup-plugin-visualizer": "^5.8.2",
|
"rollup-plugin-visualizer": "^5.8.2",
|
||||||
"stylelint": "^16.21.0",
|
"stylelint": "^16.21.0",
|
||||||
"stylelint-config-recommended-vue": "^1.5.0",
|
"stylelint-config-recommended-vue": "^1.5.0",
|
||||||
"stylelint-config-standard": "^36.0.1",
|
"stylelint-config-standard": "^36.0.1",
|
||||||
"stylelint-order": "^6.0.4",
|
"stylelint-order": "^6.0.4",
|
||||||
|
"tailwindcss": "^4.1.13",
|
||||||
"terser": "^5.43.1",
|
"terser": "^5.43.1",
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.8.4",
|
||||||
"unplugin-vue-components": "^0.24.1",
|
"unplugin-vue-components": "^0.24.1",
|
||||||
|
|
@ -93,6 +101,7 @@
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-imagemin": "^0.6.1",
|
"vite-plugin-imagemin": "^0.6.1",
|
||||||
"vite-svg-loader": "^3.6.0",
|
"vite-svg-loader": "^3.6.0",
|
||||||
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-tsc": "^1.0.14"
|
"vue-tsc": "^1.0.14"
|
||||||
},
|
},
|
||||||
"engines": {
|
"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,
|
CreateTemplateSettingParam,
|
||||||
ContractWordFormParam,
|
ContractWordFormParam,
|
||||||
ContractWordSettingListRes,
|
ContractWordSettingListRes,
|
||||||
|
SettingContractFileInfoRes,
|
||||||
} from '@/types/setting';
|
} from '@/types/setting';
|
||||||
import { PaginationParams, TenantDictionaryType } from '@/types/comm';
|
import { PaginationParams, TenantDictionaryType } from '@/types/comm';
|
||||||
import { HttpResponse } from './interceptor';
|
import { HttpResponse } from './interceptor';
|
||||||
|
|
@ -346,7 +347,7 @@ export function getCodeSettingList(
|
||||||
return axios.post(`contractSetting/getCodeSettingList`, params);
|
return axios.post(`contractSetting/getCodeSettingList`, params);
|
||||||
}
|
}
|
||||||
// 合同设置-合同条款设置-全部启用列表
|
// 合同设置-合同条款设置-全部启用列表
|
||||||
export function getAllTermsSettingList(): Promise<SettingContractNumbeRes> {
|
export function getAllTermsSettingList(): Promise<any> {
|
||||||
return axios.get(`contractSetting/getAllTermsSettingList`);
|
return axios.get(`contractSetting/getAllTermsSettingList`);
|
||||||
}
|
}
|
||||||
// 合同设置-合同条款设置-新增
|
// 合同设置-合同条款设置-新增
|
||||||
|
|
@ -416,6 +417,10 @@ export function getUseFeeList(
|
||||||
): Promise<FeeManagementListRes> {
|
): Promise<FeeManagementListRes> {
|
||||||
return axios.post(`financeSetting/getUseFeeList`, params);
|
return axios.post(`financeSetting/getUseFeeList`, params);
|
||||||
}
|
}
|
||||||
|
// 合同设置-流水账户管理--获取下拉列表
|
||||||
|
export function getUseTurnoverAccountList(params: any): Promise<any> {
|
||||||
|
return axios.get(`financeSetting/getUseTurnoverAccountList`, params);
|
||||||
|
}
|
||||||
|
|
||||||
// 合同设置-合同模板管理-全部启用列表
|
// 合同设置-合同模板管理-全部启用列表
|
||||||
export function getAllTemplateSettingList(): Promise<SettingContractNumbeRes> {
|
export function getAllTemplateSettingList(): Promise<SettingContractNumbeRes> {
|
||||||
|
|
@ -428,19 +433,15 @@ export function createTemplateSetting(
|
||||||
return axios.post(`contractSetting/createTemplateSetting`, params);
|
return axios.post(`contractSetting/createTemplateSetting`, params);
|
||||||
}
|
}
|
||||||
// 合同设置-合同模板管理-编辑
|
// 合同设置-合同模板管理-编辑
|
||||||
export function updateTemplateSetting(
|
export function updateTemplateSetting(params: any): Promise<HttpResponse> {
|
||||||
params: CreateTemplateSettingParam
|
return axios.post(`contractSetting/saveTemplateSetting`, params);
|
||||||
): Promise<HttpResponse> {
|
|
||||||
return axios.post(`contractSetting/updateTemplateSetting`, params);
|
|
||||||
}
|
}
|
||||||
// 合同设置-合同模板管理-删除
|
// 合同设置-合同模板管理-删除
|
||||||
export function delTemplateSetting(id: number): Promise<HttpResponse> {
|
export function delTemplateSetting(id: number): Promise<HttpResponse> {
|
||||||
return axios.get(`contractSetting/delTemplateSetting?id=${id}`);
|
return axios.get(`contractSetting/delTemplateSetting?id=${id}`);
|
||||||
}
|
}
|
||||||
// 合同设置-合同模板管理-单个详情
|
// 合同设置-合同模板管理-单个详情
|
||||||
export function getTemplateSettingFromId(
|
export function getTemplateSettingFromId(id: number): Promise<any> {
|
||||||
id: number
|
|
||||||
): Promise<SettingContractNumbeRes> {
|
|
||||||
return axios.get(`contractSetting/getTemplateSetting?id=${id}`);
|
return axios.get(`contractSetting/getTemplateSetting?id=${id}`);
|
||||||
}
|
}
|
||||||
// 合同设置-合同模板管理-启用/禁用
|
// 合同设置-合同模板管理-启用/禁用
|
||||||
|
|
@ -479,7 +480,7 @@ export function delWordSetting(id: number): Promise<HttpResponse> {
|
||||||
// 合同设置-合同文档模板管理-单个详情
|
// 合同设置-合同文档模板管理-单个详情
|
||||||
export function getWordSettingFromId(
|
export function getWordSettingFromId(
|
||||||
id: number
|
id: number
|
||||||
): Promise<SettingContractNumbeRes> {
|
): Promise<SettingContractFileInfoRes> {
|
||||||
return axios.get(`contractSetting/getWordSetting?id=${id}`);
|
return axios.get(`contractSetting/getWordSetting?id=${id}`);
|
||||||
}
|
}
|
||||||
// 合同设置-合同文档模板管理-启用/禁用
|
// 合同设置-合同文档模板管理-启用/禁用
|
||||||
|
|
@ -494,3 +495,47 @@ export function getWordSettingList(
|
||||||
): Promise<ContractWordSettingListRes> {
|
): Promise<ContractWordSettingListRes> {
|
||||||
return axios.post(`contractSetting/getWordSettingList`, params);
|
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 Breadcrumb from './breadcrumb/index.vue';
|
||||||
import DataTable from './common/DataTable.vue';
|
import DataTable from './common/DataTable.vue';
|
||||||
import ImageUpload from './common/ImageUpload.vue';
|
import ImageUpload from './common/ImageUpload.vue';
|
||||||
|
import OnlyOfficeEditor from './common/OnlyOfficeEditor.vue';
|
||||||
|
|
||||||
// Manually introduce ECharts modules to reduce packing size
|
// Manually introduce ECharts modules to reduce packing size
|
||||||
|
|
||||||
|
|
@ -35,5 +36,6 @@ export default {
|
||||||
Vue.component('Breadcrumb', Breadcrumb);
|
Vue.component('Breadcrumb', Breadcrumb);
|
||||||
Vue.component('DataTable', DataTable);
|
Vue.component('DataTable', DataTable);
|
||||||
Vue.component('ImageUpload', ImageUpload);
|
Vue.component('ImageUpload', ImageUpload);
|
||||||
|
Vue.component('OnlyOfficeEditor', OnlyOfficeEditor);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,158 +1,23 @@
|
||||||
<script lang="tsx">
|
<!-- src/components/menu/index.vue -->
|
||||||
import { defineComponent, ref, h, compile, computed } from 'vue';
|
<template>
|
||||||
import { useI18n } from 'vue-i18n';
|
<!-- 直接使用 TSX 组件,与普通 Vue 组件无区别 -->
|
||||||
import { useRoute, useRouter, RouteRecordRaw } from 'vue-router';
|
<MenuContent />
|
||||||
import type { RouteMeta } from 'vue-router';
|
</template>
|
||||||
import { useAppStore } from '@/store';
|
|
||||||
import { listenerRouteChange } from '@/utils/route-listener';
|
|
||||||
import { openWindow, regexUrl } from '@/utils';
|
|
||||||
import useMenuTree from './use-menu-tree';
|
|
||||||
|
|
||||||
export default defineComponent({
|
<!-- 替换原 TSX 脚本,改为导入逻辑 -->
|
||||||
emit: ['collapse'],
|
<script setup lang="ts">
|
||||||
setup() {
|
// 导入独立的 TSX 组件
|
||||||
const { t } = useI18n();
|
import MenuContent from './menu';
|
||||||
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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- 保留原有的样式,无需修改 -->
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
:deep(.arco-menu-inner) {
|
:deep(.arco-menu-inner) {
|
||||||
.arco-menu-inline-header {
|
.arco-menu-inline-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-icon {
|
.arco-icon {
|
||||||
&:not(.arco-icon-down) {
|
&:not(.arco-icon-down) {
|
||||||
font-size: 18px;
|
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' {
|
declare module '*.vue' {
|
||||||
import { DefineComponent } from 'vue';
|
import { DefineComponent } from 'vue';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
|
||||||
const component: DefineComponent<{}, {}, any>;
|
const component: DefineComponent<{}, {}, any>;
|
||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@ export default function usePermission() {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
return {
|
return {
|
||||||
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
|
accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
|
||||||
return true
|
return true;
|
||||||
// 如果路由不需要认证,直接允许访问
|
// 如果路由不需要认证,直接允许访问
|
||||||
// if (!route.meta?.requiresAuth) {
|
// if (!route.meta?.requiresAuth) {
|
||||||
// return true;
|
// return true;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // 如果用户没有权限数据,拒绝访问
|
// // 如果用户没有权限数据,拒绝访问
|
||||||
// if (!userStore.permission || userStore.permission.length === 0) {
|
// if (!userStore.permission || userStore.permission.length === 0) {
|
||||||
// return false;
|
// return false;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // 检查用户是否有该路由的权限
|
// // 检查用户是否有该路由的权限
|
||||||
// return userStore.permission.some(
|
// return userStore.permission.some(
|
||||||
// (p: any) =>
|
// (p: any) =>
|
||||||
|
|
@ -29,14 +29,15 @@ export default function usePermission() {
|
||||||
if (!permissions || permissions.length === 0) {
|
if (!permissions || permissions.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloneRouters = [..._routers];
|
const cloneRouters = [..._routers];
|
||||||
while (cloneRouters.length) {
|
while (cloneRouters.length) {
|
||||||
const firstElement = cloneRouters.shift();
|
const firstElement = cloneRouters.shift();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
permissions.some(
|
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 };
|
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 locale404 from '@/views/exception/404/locale/en-US';
|
||||||
import locale500 from '@/views/exception/500/locale/en-US';
|
import locale500 from '@/views/exception/500/locale/en-US';
|
||||||
|
|
||||||
|
|
||||||
import localeSettings from './en-US/settings';
|
import localeSettings from './en-US/settings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import App from './App.vue';
|
||||||
// https://arco.design/docs/designlab/use-theme-package
|
// https://arco.design/docs/designlab/use-theme-package
|
||||||
import '@/assets/style/global.less';
|
import '@/assets/style/global.less';
|
||||||
import '@/api/interceptor';
|
import '@/api/interceptor';
|
||||||
|
import '@/assets/style/index.css';
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(ArcoVue, {});
|
app.use(ArcoVue, {});
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ export default function setupPermissionGuard(router: Router) {
|
||||||
next();
|
next();
|
||||||
} else next(NOT_FOUND);
|
} else next(NOT_FOUND);
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line no-lonely-if
|
|
||||||
if (permissionsAllow) next();
|
if (permissionsAllow) next();
|
||||||
else {
|
else {
|
||||||
const destination =
|
const destination =
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ const RESULT: AppRouteRecordRaw = {
|
||||||
path: '/result',
|
path: '/result',
|
||||||
name: 'result',
|
name: 'result',
|
||||||
component: DEFAULT_LAYOUT,
|
component: DEFAULT_LAYOUT,
|
||||||
redirect:'/result/success',
|
redirect: '/result/success',
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.result',
|
locale: 'menu.result',
|
||||||
icon: 'icon-check-circle',
|
icon: 'icon-check-circle',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
order: 5,
|
order: 5,
|
||||||
isFirst:true,
|
isFirst: true,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ const USER: AppRouteRecordRaw = {
|
||||||
path: '/user',
|
path: '/user',
|
||||||
name: 'user',
|
name: 'user',
|
||||||
component: DEFAULT_LAYOUT,
|
component: DEFAULT_LAYOUT,
|
||||||
redirect:'/user/info',
|
redirect: '/user/info',
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.user',
|
locale: 'menu.user',
|
||||||
icon: 'icon-user',
|
icon: 'icon-user',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
order: 7,
|
order: 7,
|
||||||
isFirst:true,
|
isFirst: true,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ const VISUALIZATION: AppRouteRecordRaw = {
|
||||||
path: '/visualization',
|
path: '/visualization',
|
||||||
name: 'visualization',
|
name: 'visualization',
|
||||||
component: DEFAULT_LAYOUT,
|
component: DEFAULT_LAYOUT,
|
||||||
redirect:'/visualization/data-analysis',
|
redirect: '/visualization/data-analysis',
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.visualization',
|
locale: 'menu.visualization',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
icon: 'icon-apps',
|
icon: 'icon-apps',
|
||||||
order: 1,
|
order: 1,
|
||||||
isFirst:true,
|
isFirst: true,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const PROPERTY: AppRouteRecordRaw = {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
icon: 'icon-list',
|
icon: 'icon-list',
|
||||||
order: 4,
|
order: 4,
|
||||||
isFirst:true,
|
isFirst: true,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -118,13 +118,55 @@ const SETTING: AppRouteRecordRaw = {
|
||||||
{
|
{
|
||||||
path: 'contract',
|
path: 'contract',
|
||||||
name: 'system-setting-contract',
|
name: 'system-setting-contract',
|
||||||
component: () =>
|
redirect: '/setting/system/contract/setting',
|
||||||
import('@/views/setting/system/contract-settings/index.vue'),
|
component: PageLayout,
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'setting.contract',
|
locale: 'setting.contract',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
roles: ['*'],
|
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[];
|
contractWordSetting: ContractWordItem[];
|
||||||
page?: PaginationParams & PaginationResult;
|
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 {
|
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) {
|
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 { Message } from '@arco-design/web-vue';
|
||||||
import useDataTable from '@/composables/useDataTable';
|
import useDataTable from '@/composables/useDataTable';
|
||||||
import {
|
import {
|
||||||
getWordSettingFromId,
|
|
||||||
delWordSetting,
|
delWordSetting,
|
||||||
getWordSettingList,
|
getWordSettingList,
|
||||||
disableWordSetting,
|
disableWordSetting,
|
||||||
|
|
@ -72,7 +71,7 @@
|
||||||
import { camelToSnake } from '@/utils/caseConvert';
|
import { camelToSnake } from '@/utils/caseConvert';
|
||||||
// import contractNumberFormDrawer from './drawer/contract-number-form-drawer.vue';
|
// import contractNumberFormDrawer from './drawer/contract-number-form-drawer.vue';
|
||||||
import contractFileForm from './modal/contract-file-form.vue';
|
import contractFileForm from './modal/contract-file-form.vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
const {
|
const {
|
||||||
data: renderData,
|
data: renderData,
|
||||||
columns: projectColumn,
|
columns: projectColumn,
|
||||||
|
|
@ -91,7 +90,7 @@
|
||||||
initialPageSize: 10,
|
initialPageSize: 10,
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
});
|
});
|
||||||
|
const router = useRouter();
|
||||||
const contractWordList = ref<ContractWordItem[]>([]);
|
const contractWordList = ref<ContractWordItem[]>([]);
|
||||||
const departmentListData = ref<DepartmentItem[]>([]);
|
const departmentListData = ref<DepartmentItem[]>([]);
|
||||||
const showModal = ref<boolean>(false);
|
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 = () => {
|
const handleAddContractNumber = () => {
|
||||||
// currentNumberInfo.value = null;
|
// currentNumberInfo.value = null;
|
||||||
isEdit.value = false;
|
isEdit.value = false;
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
};
|
};
|
||||||
const handleEditContractNumber = async (id: number) => {
|
const handleEditContractNumber = async (id: number) => {
|
||||||
await getWordSettingFromIdData(id);
|
router.push({
|
||||||
isEdit.value = true;
|
name: 'system-setting-contract-file-edit',
|
||||||
// currentNumberInfo.value = info
|
query: { id },
|
||||||
showModal.value = true;
|
});
|
||||||
};
|
};
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
onMounted(() => {
|
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>
|
</DataTable>
|
||||||
<contractTemplateForm
|
<contractTemplateForm
|
||||||
v-model:visible="showModal"
|
v-model:visible="showModal"
|
||||||
|
:is-edit="false"
|
||||||
:department-list-data="departmentListData"
|
:department-list-data="departmentListData"
|
||||||
:is-edit="isEdit"
|
|
||||||
@submitted="getTemplateSettingListData"
|
|
||||||
/>
|
/>
|
||||||
</a-card>
|
</a-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import type { TableColumnData } from '@arco-design/web-vue';
|
import type { TableColumnData } from '@arco-design/web-vue';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import useDataTable from '@/composables/useDataTable';
|
import useDataTable from '@/composables/useDataTable';
|
||||||
import {
|
import {
|
||||||
getTemplateSettingFromId,
|
|
||||||
delTemplateSetting,
|
delTemplateSetting,
|
||||||
getTemplateSettingList,
|
getTemplateSettingList,
|
||||||
getDepartmentList,
|
getDepartmentList,
|
||||||
|
|
@ -69,10 +68,8 @@
|
||||||
} from '@/api/setting';
|
} from '@/api/setting';
|
||||||
import { ContractTemplateSettingItem, DepartmentItem } from '@/types/setting';
|
import { ContractTemplateSettingItem, DepartmentItem } from '@/types/setting';
|
||||||
import { UserStatus } from '@/types/comm';
|
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';
|
import contractTemplateForm from './modal/contract-template-form.vue';
|
||||||
|
const router = useRouter();
|
||||||
const {
|
const {
|
||||||
data: renderData,
|
data: renderData,
|
||||||
columns: projectColumn,
|
columns: projectColumn,
|
||||||
|
|
@ -96,7 +93,7 @@
|
||||||
const departmentListData = ref<DepartmentItem[]>([]);
|
const departmentListData = ref<DepartmentItem[]>([]);
|
||||||
const showModal = ref<boolean>(false);
|
const showModal = ref<boolean>(false);
|
||||||
const isEdit = ref<boolean>(false);
|
const isEdit = ref<boolean>(false);
|
||||||
const currentNumberInfo = ref<any>(null);
|
const currentContractTemplateInfo = ref<any>(null);
|
||||||
// 列配置
|
// 列配置
|
||||||
const columns: TableColumnData[] = [
|
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 = () => {
|
const handleAddContractNumber = () => {
|
||||||
// currentNumberInfo.value = null;
|
currentContractTemplateInfo.value = null;
|
||||||
isEdit.value = false;
|
isEdit.value = false;
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
};
|
};
|
||||||
const handleEditContractNumber = async (id: number) => {
|
const handleEditContractNumber = async (id: number) => {
|
||||||
await getTemplateSettingFromIdData(id);
|
router.push({
|
||||||
isEdit.value = true;
|
name: 'system-setting-contract-template-edit',
|
||||||
// currentNumberInfo.value = info
|
query: { id },
|
||||||
showModal.value = true;
|
});
|
||||||
};
|
};
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
onMounted(() => {
|
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>
|
<template>
|
||||||
<a-modal
|
<a-modal
|
||||||
:visible="visible"
|
:visible="visible"
|
||||||
:title="isEdit ? '编辑创建合同模板' : '新增创建合同模板'"
|
title="新增创建合同模板"
|
||||||
unmount-on-close
|
unmount-on-close
|
||||||
@ok="handleSubmit"
|
@ok="handleSubmit"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
import { createTemplateSetting, updateTemplateSetting } from '@/api/setting';
|
import { createTemplateSetting, updateTemplateSetting } from '@/api/setting';
|
||||||
import { DepartmentItem, CreateTemplateSettingParam } from '@/types/setting';
|
import { DepartmentItem, CreateTemplateSettingParam } from '@/types/setting';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
interface TreeNode {
|
interface TreeNode {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
isEdit: boolean;
|
isEdit: boolean;
|
||||||
record?: DepartmentItem | null;
|
record?: any;
|
||||||
departmentListData: TreeNode[]; // 修复类型
|
departmentListData: TreeNode[]; // 修复类型
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
@ -54,13 +54,13 @@
|
||||||
(e: 'update:visible', val: boolean): void;
|
(e: 'update:visible', val: boolean): void;
|
||||||
(e: 'submitted'): void;
|
(e: 'submitted'): void;
|
||||||
}>();
|
}>();
|
||||||
|
const router = useRouter();
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const formRef = ref<FormInstance>();
|
const formRef = ref<FormInstance>();
|
||||||
const form = reactive<CreateTemplateSettingParam>({
|
const form = reactive<any>({
|
||||||
name: '',
|
name: '',
|
||||||
departmentId: null,
|
departmentId: null,
|
||||||
// id: undefined,
|
id: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 校验规则
|
// 校验规则
|
||||||
|
|
@ -68,19 +68,18 @@
|
||||||
name: [{ required: true, message: '请输入模板名称' }],
|
name: [{ required: true, message: '请输入模板名称' }],
|
||||||
departmentId: [{ required: true, message: '请选择适用范围' }],
|
departmentId: [{ required: true, message: '请选择适用范围' }],
|
||||||
};
|
};
|
||||||
|
// 表单事件
|
||||||
// Watch 弹窗控制 & 初始化
|
const handleCancel = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
if (props.record) {
|
if (props.record) {
|
||||||
// form.id = props.record?.id;
|
Object.assign(form, {
|
||||||
// form.name = props.record?.name;
|
...props.record,
|
||||||
// form.parent_id = props.record?.parentId ?? '';
|
});
|
||||||
// form.user_id = props.record?.UserId ?? '';
|
|
||||||
// form.remark = props.record?.remark ?? '';
|
|
||||||
// Object.assign(form, props.record);
|
|
||||||
} else {
|
} else {
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
|
|
@ -92,21 +91,29 @@
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 表单事件
|
|
||||||
const handleCancel = () => {
|
|
||||||
emit('update:visible', false);
|
|
||||||
};
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
await formRef.value?.validate();
|
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 updateTemplateSetting(form)
|
||||||
: await createTemplateSetting(form);
|
: await createTemplateSetting(form);
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
Message.success(message);
|
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();
|
handleCancel();
|
||||||
// 通知父组件更新部门列表数据
|
|
||||||
emit('submitted');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</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">
|
<a-col :span="18">
|
||||||
<projectCard v-if="currentSettingId === 2" />
|
<projectCard v-if="currentSettingId === 2" />
|
||||||
<roomCard v-if="currentSettingId === 3" />
|
<roomCard v-if="currentSettingId === 3" />
|
||||||
|
<contractCard v-if="currentSettingId === 4" />
|
||||||
<tenantCard v-if="currentSettingId === 5" />
|
<tenantCard v-if="currentSettingId === 5" />
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
@ -42,6 +43,7 @@
|
||||||
import { dataDictionaryTabs } from './contrant';
|
import { dataDictionaryTabs } from './contrant';
|
||||||
import projectCard from './components/project-card.vue';
|
import projectCard from './components/project-card.vue';
|
||||||
import roomCard from './components/room-card.vue';
|
import roomCard from './components/room-card.vue';
|
||||||
|
import contractCard from './components/contract-card.vue';
|
||||||
import tenantCard from './components/tenant-card.vue';
|
import tenantCard from './components/tenant-card.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
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