diff --git a/.env.production b/.env.production index 3770849..d177213 100644 --- a/.env.production +++ b/.env.production @@ -6,4 +6,4 @@ VUE_APP_BASE_API='' VUE_APP_UPLOAD_API='/api/upload-file' VUE_APP_PREVIEW=//view.langye.net/preview/onlinePreview VUE_APP_MODULE_NAME=oa -VUE_APP_OUTPUT_DIR='/Users/mac/Documents/朗业/2025/c-常州环境监测中心内控/cz_hjjc/public/oa' +VUE_APP_OUTPUT_DIR='../backend/public/oa/' diff --git a/.gitignore b/.gitignore index 9ad28d2..ac78af7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ tests/**/coverage/ *.ntvs* *.njsproj *.sln + +.env.development +.env.production +.env.staging \ No newline at end of file diff --git a/rebuild.sh b/rebuild.sh new file mode 100644 index 0000000..a2b06bc --- /dev/null +++ b/rebuild.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +# OA模块重新构建和部署脚本 +# 用于解决资源文件404错误 + +set -e + +echo "==========================================" +echo "OA模块重新构建和部署脚本" +echo "==========================================" + +# 获取项目根目录 +PROJECT_ROOT="/Users/weizongsong/www/cz_hjjc" +OA_DIR="$PROJECT_ROOT/cz-hjjc-oa" +OUTPUT_DIR="$PROJECT_ROOT/backend/public/oa" + +# 进入OA项目目录 +cd "$OA_DIR" + +echo "" +echo "步骤1: 清理旧的构建文件..." +if [ -d "dist" ]; then + rm -rf dist + echo "✓ 已清理 dist 目录" +else + echo "✓ dist 目录不存在,跳过清理" +fi + +echo "" +echo "步骤2: 设置环境变量并构建..." +export VUE_APP_MODULE_NAME=oa +export VUE_APP_BASE_API=/api + +echo "环境变量:" +echo " VUE_APP_MODULE_NAME=$VUE_APP_MODULE_NAME" +echo " VUE_APP_BASE_API=$VUE_APP_BASE_API" + +echo "" +echo "开始构建(这可能需要几分钟)..." +npm run build:prod + +if [ ! -d "dist" ]; then + echo "❌ 构建失败:dist 目录不存在" + exit 1 +fi + +echo "✓ 构建完成" + +echo "" +echo "步骤3: 检查构建输出..." +echo "CSS文件:" +ls -1 dist/static/css/chunk-libs.* 2>/dev/null || echo " ⚠️ 未找到 chunk-libs.css" +echo "JS文件:" +ls -1 dist/static/js/chunk-libs.* 2>/dev/null || echo " ⚠️ 未找到 chunk-libs.js" +ls -1 dist/static/js/app.* 2>/dev/null || echo " ⚠️ 未找到 app.js" + +echo "" +echo "步骤4: 备份旧文件(如果存在)..." +if [ -d "$OUTPUT_DIR" ]; then + BACKUP_DIR="${OUTPUT_DIR}.backup.$(date +%Y%m%d_%H%M%S)" + echo " 备份到: $BACKUP_DIR" + cp -r "$OUTPUT_DIR" "$BACKUP_DIR" + echo "✓ 备份完成" +else + echo "✓ 输出目录不存在,无需备份" +fi + +echo "" +echo "步骤5: 部署到服务器..." +mkdir -p "$OUTPUT_DIR" +cp -r dist/* "$OUTPUT_DIR" +echo "✓ 部署完成到: $OUTPUT_DIR" + +echo "" +echo "步骤6: 验证部署..." +if [ -f "$OUTPUT_DIR/index.html" ]; then + echo "✓ index.html 存在" + + # 检查HTML中引用的文件 + echo "" + echo "HTML中引用的资源文件:" + grep -oE 'chunk-libs\.[^"]*' "$OUTPUT_DIR/index.html" | head -2 | while read file; do + if [ -f "$OUTPUT_DIR/static/css/$file" ] || [ -f "$OUTPUT_DIR/static/js/$file" ]; then + echo " ✓ $file 存在" + else + echo " ❌ $file 不存在" + fi + done +else + echo "❌ index.html 不存在" + exit 1 +fi + +echo "" +echo "==========================================" +echo "✅ 构建和部署完成!" +echo "==========================================" +echo "" +echo "下一步操作:" +echo "1. 清除浏览器缓存(Ctrl+Shift+Delete 或 Cmd+Shift+Delete)" +echo "2. 强制刷新页面(Ctrl+F5 或 Cmd+Shift+R)" +echo "3. 访问: http://192.167.20.118:8080/oa/" +echo "" +echo "如果问题仍然存在,请检查:" +echo "- Web服务器(Nginx/Apache)的缓存配置" +echo "- 服务器上的文件权限" +echo "- 服务器日志中的错误信息" +echo "" + + + + + diff --git a/src/api/flow/index.js b/src/api/flow/index.js index 0695d50..d0d3273 100644 --- a/src/api/flow/index.js +++ b/src/api/flow/index.js @@ -320,3 +320,22 @@ export function createContract(data, isLoading = false) { isLoading }) } + +// 获取可关联的流程列表 +export function getRelationOptions(params, isLoading = false) { + return request({ + method: 'get', + url: '/api/oa/flow/relation-options', + params, + isLoading + }) +} + +// 获取流程的关联列表 +export function getRelationList(flowId, isLoading = false) { + return request({ + method: 'get', + url: `/api/oa/flow/relation-list/${flowId}`, + isLoading + }) +} diff --git a/src/components/BudgetSourcePickerField.vue b/src/components/BudgetSourcePickerField.vue index 8fb3195..0085a60 100644 --- a/src/components/BudgetSourcePickerField.vue +++ b/src/components/BudgetSourcePickerField.vue @@ -7,12 +7,19 @@ {{ displayText }} + + 预算ID: {{ budgetDataId }} + - ID: {{ normalizedValue }} + {{ normalizedValue }} 选取 @@ -21,7 +28,7 @@ -
+
+ +
部门预算
+ > + + {{ node.label }} + + + 预算: + {{ formatAmount(data.budget_amount) }} + + + 已用: + {{ formatAmount(data.used_amount || 0) }} + + + 执行率: + + {{ formatExecutionRate(data.execution_rate || 0) }} + + + + +
@@ -71,24 +103,137 @@ :expand-on-click-node="false" :current-node-key="pendingId" @node-click="onNodeClick" - /> + > + + {{ node.label }} + + + 预算: + {{ formatAmount(data.budget_amount) }} + + + 已用: + {{ formatAmount(data.used_amount || 0) }} + + + 执行率: + + {{ formatExecutionRate(data.execution_rate || 0) }} + + + + + +
+ +
+
专项资金
+ + + {{ node.label }} + + + 预算: + {{ formatAmount(data.budget_amount) }} + + + 已用: + {{ formatAmount(data.used_amount || 0) }} + + + 执行率: + + {{ formatExecutionRate(data.execution_rate || 0) }} + + + + +
暂无数据
+
+ + +
+
+ 部门金额分配 +
+
+ 该预算来源授权给多个部门,请将支付总额分配到各个部门(分配金额可以为0): +
+ + + + + + +
+
+ 已分配总额: + + {{ formatAmount(allocationTotal) }} + +
+
+ 支付总额: + {{ formatAmount(totalAmount) }} +
+
+ 剩余金额: + + {{ formatAmount(remainingAmount) }} + +
+
+ 提示:当前支付总额为0,您可以在财务确认时填写实际金额 +
+
+
+
@@ -100,7 +245,7 @@ export default { name: "BudgetSourcePickerField", props: { fieldName: { type: String, required: true }, - value: { type: [String, Number], default: "" }, + value: { type: [String, Number, Object, Array], default: "" }, display: { type: String, default: "" }, }, data() { @@ -112,19 +257,123 @@ export default { yearId: null, treeData: [], pendingId: null, + // 分配相关 + pendingAuthorizations: [], + pendingAllocations: [], + totalAmount: 0, }; }, computed: { + // 解析JSON格式的value,兼容旧格式 + parsedValue() { + const value = this.value; + if (!value || value === null || value === undefined) { + return { budget_data_id: null, allocations: null }; + } + + // 已经是对象(推荐的新格式:直接对象落库/回传) + if (typeof value === "object" && !Array.isArray(value)) { + return { + budget_data_id: value?.budget_data_id ?? null, + allocations: value?.allocations ?? null, + }; + } + + // 兼容:如果是数组(极端旧数据/异常数据),当作 allocations + if (Array.isArray(value)) { + return { budget_data_id: null, allocations: value }; + } + + // 字符串:可能是 + // 1) 旧格式: "123" + // 2) 新格式: '{"budget_data_id":"2","allocations":[...]}' + // 3) 双重编码: '"{\"budget_data_id\":\"2\",\"allocations\":[...]}"' + if (typeof value === "string") { + const raw = value.trim(); + if (!raw) return { budget_data_id: null, allocations: null }; + + const parseMaybeJson = (s) => { + try { + return { ok: true, data: JSON.parse(s) }; + } catch (e) { + return { ok: false, data: null }; + } + }; + + const p1 = parseMaybeJson(raw); + if (p1.ok) { + // 解析结果是对象:标准新格式 + if (p1.data && typeof p1.data === "object" && !Array.isArray(p1.data)) { + return { + budget_data_id: p1.data?.budget_data_id ?? null, + allocations: p1.data?.allocations ?? null, + }; + } + // 解析结果是数组:当作 allocations(仍可能缺 budget_data_id) + if (Array.isArray(p1.data)) { + return { budget_data_id: null, allocations: p1.data }; + } + // 解析结果是字符串:双重编码常见形态 + if (typeof p1.data === "string") { + const inner = p1.data.trim(); + // inner 可能是 '{...}' 或者 '2' + const p2 = parseMaybeJson(inner); + if (p2.ok && p2.data && typeof p2.data === "object" && !Array.isArray(p2.data)) { + return { + budget_data_id: p2.data?.budget_data_id ?? null, + allocations: p2.data?.allocations ?? null, + }; + } + if (p2.ok && Array.isArray(p2.data)) { + return { budget_data_id: null, allocations: p2.data }; + } + // inner 可能是数字字符串 + return { + budget_data_id: isNaN(inner) ? null : String(inner), + allocations: null, + }; + } + } + + // 非JSON:当作旧格式 ID + return { budget_data_id: isNaN(raw) ? null : String(raw), allocations: null }; + } + + return { budget_data_id: null, allocations: null }; + }, + budgetDataId() { + return this.parsedValue.budget_data_id; + }, + allocations() { + return this.parsedValue.allocations; + }, normalizedValue() { - return this.value === null || this.value === undefined ? "" : String(this.value); + // 返回JSON格式的字符串(用于表单提交/兜底) + if (!this.budgetDataId) return ""; + return JSON.stringify({ + budget_data_id: this.budgetDataId, + allocations: this.allocations, + }); }, hasValue() { - return this.normalizedValue !== ""; + return this.budgetDataId !== null && this.budgetDataId !== ""; }, displayText() { - // 可写且有值时:只展示后端返回的 `{字段名}_display` - // 不再回退到原始 id,避免出现 “1年” 这种展示 - return this.hasValue ? (this.display || "") : ""; + // 优先使用后端返回的 `{字段名}_display` + if (this.display && this.display.trim()) { + return this.display; + } + + // 如果display为空,但有值,尝试从JSON中解析并生成显示文本 + if (this.hasValue && this.budgetDataId) { + // 如果value是JSON格式但display为空,显示budget_data_id作为fallback + // 注意:这里不显示原始JSON,而是显示解析出的ID + return `预算ID: ${this.budgetDataId}`; + } + + // 如果value存在但不是JSON格式(旧格式),也不显示原始值 + // 避免显示原始JSON字符串 + return ""; }, treeProps() { return { @@ -134,11 +383,31 @@ export default { }; }, departmentTree() { - return (this.treeData || []).filter((n) => n?.budget_type !== "project"); + return (this.treeData || []).filter((n) => n?.budget_type === "department"); }, projectTree() { return (this.treeData || []).filter((n) => n?.budget_type === "project"); }, + specialFundTree() { + return (this.treeData || []).filter((n) => n?.budget_type === "special_fund"); + }, + budgetTypeMap() { + return { + department: "部门预算", + project: "自有账户", + special_fund: "专项资金", + }; + }, + // 分配总额 + allocationTotal() { + return (this.pendingAllocations || []).reduce((sum, item) => { + return sum + (parseFloat(item.allocated_amount) || 0); + }, 0); + }, + // 剩余金额 + remainingAmount() { + return this.totalAmount - this.allocationTotal; + }, }, methods: { openDialog() { @@ -181,11 +450,11 @@ export default { } }, async tryResolveYearIdByValue() { - if (!this.hasValue) return null; + if (!this.hasValue || !this.budgetDataId) return null; try { const detail = await request({ method: "get", - url: `/api/budget/budget-data/${this.normalizedValue}`, + url: `/api/budget/budget-data/${this.budgetDataId}`, isLoading: false, }); const yearId = detail?.year_id ?? null; @@ -194,6 +463,18 @@ export default { return null; } }, + async getDepartmentAuthorizations(budgetDataId) { + try { + const res = await request({ + method: "get", + url: `/api/budget/budget-data/${budgetDataId}/department-authorizations`, + isLoading: false, + }); + return Array.isArray(res) ? res : []; + } catch (e) { + return []; + } + }, async loadTree(yearId) { if (!yearId) return; this.treeLoading = true; @@ -209,9 +490,42 @@ export default { this.treeData = []; await this.loadTree(val); }, - onNodeClick(node) { + async onNodeClick(node) { if (!node || !node.is_leaf) return; this.pendingId = node.id; + + // 选择叶子节点时,检查部门授权并显示分配界面 + const authorizations = await this.getDepartmentAuthorizations(node.id); + if (authorizations.length > 1) { + // 多部门:显示分配界面 + this.pendingAuthorizations = authorizations; + // 恢复之前的分配方案(如果有) + const existingAllocations = this.allocations || []; + if (existingAllocations.length > 0) { + // 合并:保留已有的分配,新增的部门设为0 + this.pendingAllocations = authorizations.map(auth => { + const existing = existingAllocations.find( + a => String(a.department_id) === String(auth.department_id) + ); + return { + department_id: auth.department_id, + department_name: auth.department?.name || '', + allocated_amount: existing ? (parseFloat(existing.allocated_amount) || 0) : 0 + }; + }); + } else { + // 没有之前的分配方案,创建新的 + this.pendingAllocations = authorizations.map(auth => ({ + department_id: auth.department_id, + department_name: auth.department?.name || '', + allocated_amount: 0 + })); + } + } else { + // 单部门:清空分配界面 + this.pendingAuthorizations = []; + this.pendingAllocations = []; + } }, findPathNames(tree, id, path = []) { if (!Array.isArray(tree)) return null; @@ -231,28 +545,87 @@ export default { ); return opt?.label || (this.yearId ? `${this.yearId}年` : ""); }, - confirmPick() { + async confirmPick() { if (!this.pendingId) return; + + // 验证多部门分配总额(如果totalAmount > 0) + if (this.pendingAuthorizations.length > 1 && this.totalAmount > 0) { + if (this.allocationTotal !== this.totalAmount) { + this.$message.warning(`分配总额必须等于支付总额 ${this.formatAmount(this.totalAmount)}`); + return; + } + } + + // 获取部门授权列表(用于确认) + const authorizations = await this.getDepartmentAuthorizations(this.pendingId); + + let allocations = []; + if (authorizations.length === 1) { + // 单部门:自动创建分配方案 + allocations = [{ + department_id: authorizations[0].department_id, + department_name: authorizations[0].department?.name || '', + allocated_amount: 0 // 占位,实际金额在financialConfirm时确定 + }]; + } else if (authorizations.length > 1) { + // 多部门:使用已填写的分配方案 + allocations = this.pendingAllocations || []; + } + + this.doConfirmPick(allocations); + }, + doConfirmPick(allocations) { const nodes = this.findPathNames(this.treeData, this.pendingId) || []; const names = nodes.map((n) => n?.name).filter(Boolean); const leaf = nodes[nodes.length - 1]; - const typeText = - leaf?.budget_type === "project" - ? "自有账户" - : leaf?.budget_type === "department" - ? "部门预算" - : leaf?.budget_type - ? String(leaf.budget_type) - : ""; + const typeText = this.budgetTypeMap[leaf?.budget_type] || leaf?.budget_type || ""; const yearLabel = this.getYearLabel(); const display = [yearLabel, typeText, ...names].filter(Boolean).join(" / "); + // 关键:不要再把JSON对象当“字符串”提交给后端 json 字段,否则后端会再次 json_encode 导致双重编码 + const payload = { + budget_data_id: String(this.pendingId), + allocations: allocations.length > 0 ? allocations : null, + }; + // 更新主值与展示值(展示值用于只读/回显) - this.$emit("input", String(this.pendingId)); + this.$emit("input", payload); this.$emit("update:display", display); this.dialogVisible = false; this.pendingId = null; + // 清空分配相关数据 + this.pendingAuthorizations = []; + this.pendingAllocations = []; + }, + onAllocationChange() { + // 分配金额变化时的处理(实时验证已在按钮disabled中处理) + }, + onDialogClose() { + this.pendingId = null; + // 清空分配相关数据 + this.pendingAuthorizations = []; + this.pendingAllocations = []; + }, + formatAmount(amount) { + if (amount === null || amount === undefined) return "0.00"; + const num = parseFloat(amount); + if (isNaN(num)) return "0.00"; + return num.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + }, + formatExecutionRate(rate) { + if (rate === null || rate === undefined) return "0.00%"; + const num = parseFloat(rate); + if (isNaN(num)) return "0.00%"; + return num.toFixed(2) + "%"; + }, + getExecutionRateClass(rate) { + const num = parseFloat(rate); + if (isNaN(num)) return ""; + if (num >= 100) return "rate-high"; + if (num >= 80) return "rate-medium"; + if (num >= 50) return "rate-normal"; + return "rate-low"; }, clearValue() { this.$emit("input", ""); @@ -292,6 +665,75 @@ export default { padding-bottom: 8px; border-bottom: 1px solid #e4e7ed; } + +.custom-tree-node { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 14px; + padding-right: 8px; +} + +.node-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.node-usage-info { + display: flex; + align-items: center; + gap: 12px; + margin-left: 12px; + flex-shrink: 0; + font-size: 12px; +} + +.usage-item { + display: flex; + align-items: center; + gap: 4px; +} + +.usage-label { + color: #909399; + font-size: 12px; +} + +.usage-value { + color: #606266; + font-weight: 500; + font-size: 12px; +} + +.usage-value--used { + color: #409eff; +} + +.usage-value--rate { + min-width: 50px; + text-align: right; +} + +.usage-value--rate.rate-high { + color: #f56c6c; + font-weight: 600; +} + +.usage-value--rate.rate-medium { + color: #e6a23c; + font-weight: 500; +} + +.usage-value--rate.rate-normal { + color: #409eff; +} + +.usage-value--rate.rate-low { + color: #909399; +} diff --git a/src/components/ContractSignField.vue b/src/components/ContractSignField.vue index ffd6a9f..2af40f5 100644 --- a/src/components/ContractSignField.vue +++ b/src/components/ContractSignField.vue @@ -22,7 +22,7 @@ - - + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + - - + + + + - - + + + + + + + + + + + + + + + + + + - - + + + 上传文件 + + 付款计划
新增计划
- +