zhangwencui
2 天以前 f57f284cc15b62bb8928bf555fa40bde4c1c7eff
1、列表权限放开。2、增加报工审批
已修改4个文件
91 ■■■■■ 文件已修改
APP_DEVELOPMENT_REQUIREMENTS.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/mes/pro/feedback/form.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/mes/pro/feedback/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/auth.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
APP_DEVELOPMENT_REQUIREMENTS.md
@@ -17,7 +17,7 @@
## 权限规则
- 页面查看权限使用 Web 端列表查询权限,例如 `xxx:query`。
- 页面查看权限优先使用 Web 端列表查询权限,例如 `xxx:query`;当前用户拥有对应菜单时,也视为有页面查看权限。
- 新增、编辑、删除、提交、状态变更等按钮必须按 Web 端同名权限控制。
- 没有权限时不展示按钮;没有页面查询权限时展示无权限提示。
src/pages/mes/pro/feedback/form.vue
@@ -5,13 +5,13 @@
import type { UserSimple } from "../../../../api/purchase";
import { getSupplierSimpleList, type SupplierItem } from "../../../../api/supplier";
import { generateAutoCode, getWorkstationPage, type MesWorkstation } from "../../../../api/process-design";
import { createFeedback, getBatchRequirements, getFeedback, getTaskPage, getWorkOrder, getWorkOrderPage, getWorkOrderParamRecordPage, submitFeedback, updateFeedback, type FeedbackBatchRequirement, type ProFeedback, type ProTask, type WorkOrder, type WorkOrderParamRecordDetail } from "../../../../api/production";
import { approveFeedback, createFeedback, getBatchRequirements, getFeedback, getTaskPage, getWorkOrder, getWorkOrderPage, getWorkOrderParamRecordPage, rejectFeedback, submitFeedback, updateFeedback, type FeedbackBatchRequirement, type ProFeedback, type ProTask, type WorkOrder, type WorkOrderParamRecordDetail } from "../../../../api/production";
import { useAuth } from "../../../../stores/auth";
import { dateText, dictLabel, dictOptions } from "../../dv/shared";
const { hasPermission, state } = useAuth();
const id = ref<number>();
const mode = ref<"edit" | "detail" | "submit">("edit");
const mode = ref<"edit" | "detail" | "submit" | "approve">("edit");
const saving = ref(false);
const generating = ref(false);
const dicts = ref<DictData[]>([]);
@@ -24,9 +24,10 @@
const paramDetails = ref<WorkOrderParamRecordDetail[]>([]);
const batchReq = ref<FeedbackBatchRequirement>({});
const batch = reactive<Record<string, string | number | undefined>>({});
const readonly = computed(() => mode.value === "detail" || mode.value === "submit");
const readonly = computed(() => mode.value === "detail" || mode.value === "submit" || mode.value === "approve");
const canSave = computed(() => !readonly.value && (id.value ? hasPermission("mes:pro-feedback:update") : hasPermission("mes:pro-feedback:create")));
const canSubmit = computed(() => id.value && form.status === 0 && hasPermission("mes:pro-feedback:update"));
const canApprove = computed(() => mode.value === "approve" && form.status === 2 && form.approveUserId === state.user?.id && hasPermission("mes:pro-feedback:approve"));
const feedbackTypes = computed(() => dictOptions(dicts.value, "mes_pro_feedback_type"));
const qualityOptions = computed(() => dictOptions(dicts.value, "mes_wm_quality_status"));
const taskOptions = computed(() => tasks.value.filter(item => !form.workOrderId || item.workOrderId === form.workOrderId));
@@ -199,6 +200,49 @@
    uni.showToast({ title: error instanceof Error ? error.message : "提交失败", icon: "none" });
  }
}
function validateBatch() {
  if (needExpireDate.value && !batch.expireDate) return uni.showToast({ title: "请选择有效期", icon: "none" }), false;
  if (needReceiptDate.value && !batch.receiptDate) return uni.showToast({ title: "请选择入库日期", icon: "none" }), false;
  if (needLotNumber.value && !batch.lotNumber) return uni.showToast({ title: "请输入生产批号", icon: "none" }), false;
  if (needTool.value && !batch.toolId) return uni.showToast({ title: "请输入工具ID", icon: "none" }), false;
  if (needMold.value && !batch.moldId) return uni.showToast({ title: "请输入模具ID", icon: "none" }), false;
  if (needQualityStatus.value && batch.qualityStatus === undefined) return uni.showToast({ title: "请选择质量状态", icon: "none" }), false;
  if (needVendor.value && !batch.vendorId) return uni.showToast({ title: "请选择供应商", icon: "none" }), false;
  if (needClient.value && !batch.clientId) return uni.showToast({ title: "请选择客户", icon: "none" }), false;
  if (needSalesOrderCode.value && !batch.salesOrderCode) return uni.showToast({ title: "请输入销售订单编号", icon: "none" }), false;
  if (needPurchaseOrderCode.value && !batch.purchaseOrderCode) return uni.showToast({ title: "请输入采购订单编号", icon: "none" }), false;
  return true;
}
async function approve() {
  if (!state.accessToken || !id.value || saving.value || !canApprove.value || !validateBatch()) return;
  const confirmed = await new Promise<boolean>(resolve => uni.showModal({ title: "确认审批", content: "确认审批通过该报订单?", success: result => resolve(result.confirm) }));
  if (!confirmed) return;
  saving.value = true;
  try {
    const finished = await approveFeedback(state.accessToken, { id: id.value, ...batch });
    uni.showToast({ title: finished ? "报订单已审批完成" : "审批通过,等待质量检验", icon: "success" });
    setTimeout(() => uni.navigateBack(), 500);
  } catch (error) {
    uni.showToast({ title: error instanceof Error ? error.message : "审批失败", icon: "none" });
  } finally {
    saving.value = false;
  }
}
async function reject() {
  if (!state.accessToken || !id.value || saving.value || !canApprove.value) return;
  const confirmed = await new Promise<boolean>(resolve => uni.showModal({ title: "确认驳回", content: "确认审批不通过该报订单?", success: result => resolve(result.confirm) }));
  if (!confirmed) return;
  saving.value = true;
  try {
    await rejectFeedback(state.accessToken, id.value);
    uni.showToast({ title: "报订单已驳回", icon: "success" });
    setTimeout(() => uni.navigateBack(), 500);
  } catch (error) {
    uni.showToast({ title: error instanceof Error ? error.message : "驳回失败", icon: "none" });
  } finally {
    saving.value = false;
  }
}
async function save(stay = false) {
  if (!form.code?.trim()) return uni.showToast({ title: "请填写报订单号", icon: "none" });
  if (!form.type) return uni.showToast({ title: "请选择报工类型", icon: "none" });
@@ -234,7 +278,8 @@
}
onLoad(async query => {
  id.value = query.id ? Number(query.id) : undefined;
  mode.value = query.mode === "detail" ? "detail" : query.mode === "submit" ? "submit" : "edit";
  mode.value = query.mode === "detail" ? "detail" : query.mode === "submit" ? "submit" : query.mode === "approve" ? "approve" : "edit";
  uni.setNavigationBarTitle({ title: mode.value === "approve" ? "审批生产报工" : mode.value === "detail" ? "生产报工详情" : mode.value === "submit" ? "提交生产报工" : id.value ? "编辑生产报工" : "新增生产报工" });
  if (!state.accessToken) return;
  try {
    const [ds, taskPage, orderPage, ws, us, supplierData, customerData] = await Promise.all([
@@ -256,7 +301,7 @@
    if (id.value) {
      Object.assign(form, await getFeedback(state.accessToken, id.value));
      await loadParamDetails(form.taskId);
      if (mode.value === "submit") batchReq.value = await getBatchRequirements(state.accessToken, id.value);
      if (mode.value === "submit" || mode.value === "approve") batchReq.value = await getBatchRequirements(state.accessToken, id.value);
    }
  } catch (error) {
    uni.showToast({ title: error instanceof Error ? error.message : "数据加载失败", icon: "none" });
@@ -290,10 +335,10 @@
      <view class="section-title">工艺参数</view>
      <view v-for="item in paramDetails" :key="item.id" class="field">
        <text>{{ item.paramName || item.paramCode }}<text v-if="item.required" class="required"> *</text><text v-if="item.unitMeasureName">({{ item.unitMeasureName }})</text></text>
        <input v-model="item.paramValue" :disabled="mode==='detail'" placeholder="请输入参数值" />
        <input v-model="item.paramValue" :disabled="mode==='detail' || mode==='approve'" placeholder="请输入参数值" />
      </view>
    </view>
    <view v-if="mode==='submit' && batchReq.needBatchInput" class="form-card batch-card">
    <view v-if="(mode==='submit' || mode==='approve') && batchReq.needBatchInput" class="form-card batch-card">
      <view class="section-title">批次属性</view>
      <view v-if="needExpireDate" class="field"><text>有效期 <text class="required">*</text></text><picker mode="date" :value="String(batch.expireDate||'').slice(0,10)" @change="batch.expireDate=$event.detail.value+' 00:00:00'"><view class="picker"><text>{{ dateText(batch.expireDate).replace('--','请选择有效期') }}</text><text>›</text></view></picker></view>
      <view v-if="needReceiptDate" class="field"><text>入库日期 <text class="required">*</text></text><picker mode="date" :value="String(batch.receiptDate||'').slice(0,10)" @change="batch.receiptDate=$event.detail.value+' 00:00:00'"><view class="picker"><text>{{ dateText(batch.receiptDate).replace('--','请选择入库日期') }}</text><text>›</text></view></picker></view>
@@ -308,10 +353,14 @@
    </view>
    <button v-if="canSave" class="save" :loading="saving" :disabled="saving" @click="save()">保存</button>
    <button v-if="canSubmit" class="confirm" :loading="saving" :disabled="saving" @click="submit">提交</button>
    <view v-if="canApprove" class="approval-actions">
      <button class="reject" :loading="saving" :disabled="saving" @click="reject">不通过</button>
      <button class="confirm" :loading="saving" :disabled="saving" @click="approve">通过</button>
    </view>
    <button v-else-if="readonly && hasPermission('mes:pro-feedback:update') && form.status === 0" class="save" @click="mode='edit'">编辑</button>
  </view>
</template>
<style lang="scss" scoped>
.page{min-height:100vh;padding:24rpx 30rpx 70rpx;box-sizing:border-box;background:#f6f7f9}.form-card{overflow:hidden;background:#fff;border-radius:16rpx}.batch-card{margin-top:22rpx}.section-title{padding:24rpx;color:#172b45;font-size:31rpx;font-weight:600;border-bottom:1rpx solid #eef1f5}.field{display:flex;align-items:center;min-height:96rpx;padding:0 24rpx;gap:20rpx;border-bottom:1rpx solid #eef1f5}.field>text:first-child{width:210rpx;flex:none;color:#33455d;font-size:27rpx}.field input,.field picker,.picker,.value{min-width:0;flex:1;color:#1e3049;font-size:27rpx}.field input,.value{text-align:right}.picker{display:flex;align-items:center;justify-content:space-between;color:#65758a}.picker text:first-child,.value{overflow:hidden;text-align:right;text-overflow:ellipsis;white-space:nowrap}.picker text:last-child{margin-left:16rpx;color:#8092a7;font-size:36rpx}.required{color:#e23b3b}.code-field button{flex:none;min-width:92rpx;padding:0 12rpx;margin:0;color:var(--color-primary, #1769d8);font-size:23rpx;line-height:56rpx;background:#fff;border:1rpx solid var(--color-primary, #1769d8);border-radius:8rpx}.code-field button::after,.save::after,.confirm::after{border:0}.column{align-items:flex-start;flex-direction:column;padding:20rpx 24rpx}.column>text:first-child{width:auto}.column textarea{width:100%;height:150rpx;font-size:27rpx;box-sizing:border-box}.save,.confirm{margin-top:30rpx;color:#fff;font-size:30rpx;background:var(--color-primary, #1769d8);border-radius:12rpx}.confirm{background:#16a36a}
.page{min-height:100vh;padding:24rpx 30rpx 70rpx;box-sizing:border-box;background:#f6f7f9}.form-card{overflow:hidden;background:#fff;border-radius:16rpx}.batch-card{margin-top:22rpx}.section-title{padding:24rpx;color:#172b45;font-size:31rpx;font-weight:600;border-bottom:1rpx solid #eef1f5}.field{display:flex;align-items:center;min-height:96rpx;padding:0 24rpx;gap:20rpx;border-bottom:1rpx solid #eef1f5}.field>text:first-child{width:210rpx;flex:none;color:#33455d;font-size:27rpx}.field input,.field picker,.picker,.value{min-width:0;flex:1;color:#1e3049;font-size:27rpx}.field input,.value{text-align:right}.picker{display:flex;align-items:center;justify-content:space-between;color:#65758a}.picker text:first-child,.value{overflow:hidden;text-align:right;text-overflow:ellipsis;white-space:nowrap}.picker text:last-child{margin-left:16rpx;color:#8092a7;font-size:36rpx}.required{color:#e23b3b}.code-field button{flex:none;min-width:92rpx;padding:0 12rpx;margin:0;color:var(--color-primary, #1769d8);font-size:23rpx;line-height:56rpx;background:#fff;border:1rpx solid var(--color-primary, #1769d8);border-radius:8rpx}.code-field button::after,.save::after,.confirm::after,.reject::after{border:0}.column{align-items:flex-start;flex-direction:column;padding:20rpx 24rpx}.column>text:first-child{width:auto}.column textarea{width:100%;height:150rpx;font-size:27rpx;box-sizing:border-box}.save,.confirm,.reject{margin-top:30rpx;color:#fff;font-size:30rpx;border-radius:12rpx}.save{background:var(--color-primary, #1769d8)}.confirm{background:#16a36a}.approval-actions{display:flex;gap:20rpx}.approval-actions button{flex:1}.reject{color:#c65050;background:#fff;border:1rpx solid #e3a0a0}
</style>
src/pages/mes/pro/feedback/index.vue
@@ -68,6 +68,10 @@
  if (!row.id) return;
  uni.navigateTo({ url: `/pages/mes/pro/feedback/form?id=${row.id}&mode=submit` });
}
function goApprove(row: ProFeedback) {
  if (!row.id) return;
  uni.navigateTo({ url: `/pages/mes/pro/feedback/form?id=${row.id}&mode=approve` });
}
function goApproval(row: ProFeedback) {
  if (!row.processInstanceId) return uni.showToast({ title: "该报工尚未提交审批", icon: "none" });
  uni.navigateTo({ url: `/pages/bpm/process-instance/detail?id=${encodeURIComponent(String(row.processInstanceId))}` });
@@ -116,6 +120,7 @@
              <button @click="goForm(row.id,'detail')">详情</button>
              <button v-if="canUpdate && row.status === 0" @click="goForm(row.id)">编辑</button>
              <button v-if="canUpdate && row.status === 0" @click="submit(row)">提交</button>
              <button v-if="hasPermission('mes:pro-feedback:approve') && row.status === 2 && row.approveUserId === state.user?.id" @click="goApprove(row)">审批</button>
              <button v-if="row.status === 2" @click="goApproval(row)">查看审批</button>
              <button v-if="canDelete && row.status === 0" class="danger" @click="remove(row)">删除</button>
              <button @click="goArchive(row)">归档</button>
src/stores/auth.ts
@@ -12,6 +12,7 @@
  type LoginResult,
  type UserProfile,
} from '../api/auth';
import { appRouteMap } from '../config/app-routes';
const TOKEN_KEY = 'mom_access_token';
const REFRESH_TOKEN_KEY = 'mom_refresh_token';
@@ -86,8 +87,24 @@
export function useAuth() {
  const isLoggedIn = computed(() => Boolean(state.accessToken));
  function hasCurrentPageMenu() {
    const pages = getCurrentPages();
    const currentRoute = pages[pages.length - 1]?.route;
    if (!currentRoute) return false;
    const pagePath = `/${currentRoute.replace(/^\//, '')}`;
    const visit = (menus: AuthMenu[]): boolean =>
      menus.some(menu => {
        if (!menu.visible) return false;
        if (menu.component && appRouteMap[menu.component] === pagePath) return true;
        return menu.children?.length ? visit(menu.children) : false;
      });
    return visit(state.menus);
  }
  function hasPermission(code: string) {
    return state.permissions.includes(code);
    if (state.permissions.includes(code)) return true;
    // 列表页只要出现在当前用户菜单中即可查看;增删改等操作仍严格校验按钮权限。
    return code.endsWith(':query') && hasCurrentPageMenu();
  }
  async function initializeSession(result: LoginResult) {