185 lines
10 KiB
Vue
185 lines
10 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, ref, computed, watch } from 'vue';
|
||
import { Icon } from '@iconify/vue'
|
||
import { useThemeStore } from '@/store/modules/theme';
|
||
import { useRouterPush } from '@/hooks/common/router';
|
||
import { useRoute } from 'vue-router';
|
||
|
||
const route = useRoute();
|
||
const { routerPushByKey } = useRouterPush();
|
||
const themeStore = useThemeStore();
|
||
|
||
const sc = ref<any>(null);
|
||
const cur = ref<number>(0);
|
||
const playing = ref(false);
|
||
const curStageIdx = computed(() => (sc.value ? sc.value.stages.indexOf(sc.value.steps[cur.value].stage) : 0));
|
||
const finished = computed(() => (sc.value ? cur.value >= sc.value?.steps?.length - 1 : false));
|
||
const ROLES_NAME: Record<string, string> = {
|
||
applicant: "申请人(张工艺)", process: "专业会签(王仪表)", process2: "专业会签(周设备)",
|
||
safety: "安全工程师(王海燕)", dept: "车间主任(李主任)", vp: "分管领导(刘副总)", admin: "系统管理员",
|
||
quality: "质量工程师(陈质量)", viewer: "非审批人员(吴新)",
|
||
};
|
||
const VIEW_NAME: Record<string, string> = {
|
||
apply: "变更申请", tasks: "我的任务", approvals: "审批中心", execution: "PSSR / 验收", closure: "关闭管理",
|
||
};
|
||
|
||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||
watch([playing, cur, sc], ([p, c, s]) => {
|
||
if (timer) { clearTimeout(timer); timer = undefined; }
|
||
if (!p || !s) return;
|
||
if (c >= s.steps.length - 1) { playing.value = false; return; }
|
||
timer = setTimeout(() => { cur.value = Math.min(cur.value + 1, (sc.value?.steps.length ?? 1) - 1); }, 2000);
|
||
});
|
||
|
||
function jumpFor(s: any): { role: string; view: string } | null {
|
||
const a = s.actor;
|
||
if (a === "系统") return null;
|
||
let role: string;
|
||
if (a.includes("张工艺")) role = "applicant";
|
||
else if (a.includes("李主任")) role = "dept";
|
||
else if (a.includes("王仪表")) role = "process";
|
||
else if (a.includes("周设备")) role = "process2";
|
||
else if (a.includes("刘副总")) role = "vp";
|
||
else if (a.includes("专人") || a.includes("安全")) role = "safety";
|
||
else if (a.includes("岗位") || a.includes("维修班")) role = "applicant";
|
||
else if (a.includes("车间")) role = "dept";
|
||
else return null;
|
||
let view: string;
|
||
if (role === "applicant") view = s.stage === "变更申请" ? "apply" : "tasks";
|
||
else if (s.stage.includes("审批")) view = "approvals";
|
||
else if (s.stage === "关闭") view = "closure";
|
||
else view = "execution";
|
||
return { role, view };
|
||
}
|
||
function prev() { playing.value = false; cur.value = Math.max(0, cur.value - 1); }
|
||
function next() { playing.value = false; if (sc.value) cur.value = Math.min(sc.value.steps.length - 1, cur.value + 1); }
|
||
function reset() { playing.value = false; cur.value = 0; }
|
||
function back() {
|
||
routerPushByKey("home_process");
|
||
}
|
||
function jumpTo() {
|
||
routerPushByKey("home_initiatechange");
|
||
}
|
||
onMounted(() => {
|
||
if(route.query && route.query.params){
|
||
sc.value = JSON.parse(route.query.params as string);
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<NSpace vertical :size="16">
|
||
<!-- 流程演示详情 -->
|
||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||
<div class="space-y-3">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<NButton ghost type="primary" class="text-sm font-medium" @click="back">
|
||
<Icon icon="ep:back" class="size-16px mr-1" /> 返回示例列表
|
||
</NButton>
|
||
<span class="text-base font-bold text-slate-800">{{ sc?.no }} {{ sc?.title }}</span>
|
||
<NTag size="small" v-for="t in sc?.tags" :key="t.text" :class="t.cls">{{ t.text }}</NTag>
|
||
<span class="text-xs text-slate-400">示例变更:{{ sc?.changeName }}</span>
|
||
</div>
|
||
|
||
<!-- 阶段进度条 -->
|
||
<div class="flex items-center gap-1 overflow-x-auto rounded-lg border bg-white p-3">
|
||
<div v-for="(st, i) in sc?.stages" :key="st" class="flex shrink-0 items-center gap-1">
|
||
<span
|
||
class="flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium"
|
||
:class="i < curStageIdx ? 'bg-emerald-50 text-emerald-600' : i === curStageIdx ? 'bg-[var(--theme-color)] text-white' : 'bg-slate-100 text-slate-400'"
|
||
:style="{ '--theme-color': themeStore.themeColor }"
|
||
>
|
||
<Icon v-if="i < curStageIdx" icon="ix:success" class="size-14px" />
|
||
<span v-else class="flex h-3.5 w-3.5 items-center justify-center rounded-full border text-[10px]" :class="i === curStageIdx ? 'border-white' : 'border-slate-300'">{{ Number(i) + 1 }}</span>
|
||
{{ st }}
|
||
</span>
|
||
<Icon v-if="Number(i) < sc?.stages?.length - 1" icon="formkit:right" class="size-12px text-slate-300" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 控制条 -->
|
||
<div class="flex flex-wrap items-center gap-2 rounded-lg border bg-white px-3 py-2">
|
||
<NButton ghost size="small" class="text-xs" :disabled="cur === 0" @click="prev">
|
||
<Icon icon="formkit:left" class="size-12px" /> 上一步
|
||
</NButton>
|
||
<NButton type="primary" size="small" class="text-xs text-white" :disabled="finished" @click="next">
|
||
下一步 <Icon icon="formkit:right" class="size-12px" />
|
||
</NButton>
|
||
<NButton ghost size="small" class="text-xs" :disabled="finished && !playing" @click="playing = !playing">
|
||
<template v-if="playing"><Icon icon="material-symbols:pause-outline-rounded" class="size-14px" /> 暂停</template>
|
||
<template v-else><Icon icon="griddy-icons:play" class="size-14px" /> 自动播放</template>
|
||
</NButton>
|
||
<NButton ghost size="small" class="text-xs" @click="reset">
|
||
<Icon icon="system-uicons:reset" class="size-12px" /> 重置
|
||
</NButton>
|
||
<span class="ml-auto text-xs text-slate-400">第 {{ cur + 1 }} / {{ sc?.steps?.length }} 步{{ finished ? " · 流程已走通 ✓" : "" }}</span>
|
||
</div>
|
||
|
||
<!-- 完成横幅 -->
|
||
<div v-if="finished" class="flex flex-wrap items-center gap-2 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-2.5 text-sm text-emerald-700">
|
||
<Icon icon="ix:success" class="size-16px" />
|
||
<b>流程已走通:</b>共 {{ sc.steps.length }} 步,{{ sc.stages.length }} 个阶段全部闭环;全程操作留痕、资料归档、公告发布完整可追溯。
|
||
</div>
|
||
|
||
<!-- 步骤时间线 -->
|
||
<div class="space-y-2">
|
||
<div
|
||
v-for="(s, i) in sc?.steps" :key="i" @click="playing = false; cur = Number(i)"
|
||
class="flex w-full cursor-pointer items-start gap-3 rounded-lg border px-3 py-2.5 text-left transition"
|
||
:class="i === cur ? 'border-[var(--theme-color)] bg-[#EAF0F9]/50 shadow-sm' : Number(i) < cur ? 'border-emerald-100 bg-emerald-50/30' : 'bg-white hover:border-slate-300'"
|
||
:style="{ '--theme-color': themeStore.themeColor }"
|
||
>
|
||
<span
|
||
class="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[11px] font-bold"
|
||
:class="Number(i) < cur ? 'bg-emerald-500 text-white' : i === cur ? 'bg-[var(--theme-color)] text-white' : 'bg-slate-100 text-slate-400'"
|
||
:style="{ '--theme-color': themeStore.themeColor }"
|
||
>
|
||
<Icon v-if="Number(i) < cur" icon="ix:success" class="size-16px" />
|
||
<template v-else>{{ Number(i) + 1 }}</template>
|
||
</span>
|
||
<span class="min-w-0 flex-1">
|
||
<span class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||
<span class="flex items-center gap-1 text-xs text-slate-500">
|
||
<Icon icon="lucide:user-round" class="size-14px" />
|
||
{{ s.actor }}{{ s.role ? `(${s.role})` : "" }}
|
||
</span>
|
||
<span class="rounded bg-slate-100 px-1.5 text-[10px] text-slate-400">{{ s.stage }}</span>
|
||
<span class="text-sm font-medium" :class="i === cur ? 'text-[#17407F]' : 'text-slate-700'">{{ s.title }}</span>
|
||
<NTag v-if="s.branch" class="border-violet-200 bg-violet-50 text-violet-600">{{ s.branch }}</NTag>
|
||
<span class="ml-auto shrink-0 font-mono text-[11px] text-slate-400">{{ s.time }}</span>
|
||
<NButton ghost type="primary" size="tiny" v-if="jumpFor(s) && s.actor !== '系统'"
|
||
class="text-[10px] font-medium"
|
||
:title="`切换到${ROLES_NAME[jumpFor(s)!.role]}的「${VIEW_NAME[jumpFor(s)!.view] ?? jumpFor(s)!.view}」界面`"
|
||
@click.stop="jumpTo"
|
||
>
|
||
进入界面 →
|
||
</NButton>
|
||
</span>
|
||
<span v-if="i === cur && (s.detail || s.system)" class="mt-2 block space-y-1.5 border-t border-[#ACC6E5]/40 pt-2">
|
||
<span v-if="s.detail" class="block text-xs leading-relaxed text-slate-600"><b class="text-slate-500">内容:</b>{{ s.detail }}</span>
|
||
<span
|
||
v-if="s.system"
|
||
class="flex items-start gap-1.5 text-xs leading-relaxed"
|
||
:style="{ color: themeStore.themeColor }"
|
||
>
|
||
<Icon icon="lucide:sparkles" class="mt-0.8 size-12px" />{{ s.system }}
|
||
</span>
|
||
</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</NCard>
|
||
</NSpace>
|
||
</template>
|
||
|
||
<style scoped>
|
||
|
||
</style>
|
||
<style scoped lang="scss">
|
||
.scroll {
|
||
height: calc(100vh - 185px);
|
||
overflow-y: auto;
|
||
}
|
||
</style>
|