初步完成ai插件对接
This commit is contained in:
@@ -0,0 +1,453 @@
|
|||||||
|
/**
|
||||||
|
* ai-server OpenApi(MOC 插件)接入层
|
||||||
|
* 契约:《变更管理系统插件接口说明文档 V1.0》(ai-server Module/OpenApi)
|
||||||
|
* - base 走 vite 代理 /proxy-ai -> http://127.0.0.1:12333(见 vite.config.ts)
|
||||||
|
* - 鉴权:app 凭据换应用令牌(缓存 2h)→ exchange 换会员 JWT(ai_token,会话级缓存)
|
||||||
|
* - 本地联调形态:前端直连 exchange(moc-server 的登录双令牌下发链路就绪后,
|
||||||
|
* 改为登录时由 moc-server 下发 ai_token,本层仅需替换 getAiToken 来源)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** ai-server 统一响应 */
|
||||||
|
interface AiResp<T = any> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AiError extends Error {
|
||||||
|
code: number;
|
||||||
|
|
||||||
|
data: any;
|
||||||
|
|
||||||
|
constructor(code: number, message: string, data?: any) {
|
||||||
|
super(message || `AI 服务错误(${code})`);
|
||||||
|
this.code = code;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE = (import.meta.env.VITE_AI_BASE_URL as string) || '/proxy-ai';
|
||||||
|
const APP_ID = (import.meta.env.VITE_AI_APP_ID as string) || 'moc_jcyh';
|
||||||
|
const APP_SECRET = (import.meta.env.VITE_AI_APP_SECRET as string) || 'moc-jcyh-dev-secret';
|
||||||
|
|
||||||
|
/** exchange 用的当前 MOC 用户身份(登录成功后由调用方注入) */
|
||||||
|
export interface AiUserIdentity {
|
||||||
|
platform_user_id: string;
|
||||||
|
align_key: string;
|
||||||
|
display_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let identity: AiUserIdentity = {
|
||||||
|
// 本地联调默认用户(文档 §8.2)
|
||||||
|
platform_user_id: '1001',
|
||||||
|
align_key: 'zhang.gong@jcyh.local',
|
||||||
|
display_name: '张工'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 登录后注入真实用户身份;用户变更会强制重新 exchange */
|
||||||
|
export function setAiUser(user: Partial<AiUserIdentity>) {
|
||||||
|
identity = { ...identity, ...user };
|
||||||
|
aiTokenCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let appTokenCache: { token: string; expireAt: number } | null = null;
|
||||||
|
let aiTokenCache: { token: string; userKey: string } | null = null;
|
||||||
|
let requestSeq = 0;
|
||||||
|
|
||||||
|
async function rawFetch<T>(path: string, init: RequestInit): Promise<AiResp<T>> {
|
||||||
|
let resp: Response;
|
||||||
|
try {
|
||||||
|
resp = await fetch(`${BASE}${path}`, init);
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new AiError(-1, `AI 服务不可达:${e?.message ?? e}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (await resp.json()) as AiResp<T>;
|
||||||
|
} catch {
|
||||||
|
throw new AiError(resp.status, `AI 服务响应异常(HTTP ${resp.status})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAppToken(): Promise<string> {
|
||||||
|
if (appTokenCache && appTokenCache.expireAt > Date.now() + 60_000) return appTokenCache.token;
|
||||||
|
const body = await rawFetch<{ access_token: string; expires_in: number }>('/open/v1/auth/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET })
|
||||||
|
});
|
||||||
|
if (body.code !== 0) throw new AiError(body.code, body.message || '应用令牌获取失败', body.data);
|
||||||
|
appTokenCache = { token: body.data.access_token, expireAt: Date.now() + (body.data.expires_in ?? 7200) * 1000 };
|
||||||
|
return appTokenCache.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAiToken(): Promise<string> {
|
||||||
|
const userKey = identity.platform_user_id || identity.align_key;
|
||||||
|
if (aiTokenCache && aiTokenCache.userKey === userKey) return aiTokenCache.token;
|
||||||
|
const cached = sessionStorage.getItem(`ai_token:${userKey}`);
|
||||||
|
if (cached) {
|
||||||
|
aiTokenCache = { token: cached, userKey };
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const appToken = await getAppToken();
|
||||||
|
const body = await rawFetch<{ token: string }>('/open/v1/auth/exchange', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${appToken}` },
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform: 'moc',
|
||||||
|
platform_user_id: identity.platform_user_id,
|
||||||
|
align_key: identity.align_key,
|
||||||
|
display_name: identity.display_name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (body.code !== 0) throw new AiError(body.code, body.message || 'ai_token 签发失败', body.data);
|
||||||
|
aiTokenCache = { token: body.data.token, userKey };
|
||||||
|
sessionStorage.setItem(`ai_token:${userKey}`, body.data.token);
|
||||||
|
return body.data.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ai_token 失效(40101)时清缓存并自动重新 exchange 重试一次 */
|
||||||
|
async function aiFetch<T = any>(path: string, payload?: unknown, method: 'GET' | 'POST' = 'POST', retried = false): Promise<T> {
|
||||||
|
const token = await getAiToken();
|
||||||
|
const body = await rawFetch<T>(path, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||||
|
body: method === 'GET' ? undefined : JSON.stringify(payload ?? {})
|
||||||
|
});
|
||||||
|
if (body.code === 0) return body.data;
|
||||||
|
if (body.code === 40101 && !retried) {
|
||||||
|
aiTokenCache = null;
|
||||||
|
sessionStorage.removeItem(`ai_token:${identity.platform_user_id || identity.align_key}`);
|
||||||
|
return aiFetch<T>(path, payload, method, true);
|
||||||
|
}
|
||||||
|
throw new AiError(body.code, body.message || 'AI 分析失败', body.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextRequestId(prefix: string): string {
|
||||||
|
requestSeq += 1;
|
||||||
|
return `moc-${Date.now()}-${requestSeq}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 类型与默认载荷 ---------- */
|
||||||
|
|
||||||
|
export interface CompareRow {
|
||||||
|
item: string;
|
||||||
|
before: string;
|
||||||
|
after: string;
|
||||||
|
hl?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatrixPayload {
|
||||||
|
size: string;
|
||||||
|
l_range?: [number, number];
|
||||||
|
s_range?: [number, number];
|
||||||
|
rr?: { min: number; level: string; name: string }[];
|
||||||
|
default_level?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LevelDim {
|
||||||
|
dim: string;
|
||||||
|
name: string;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MOC 官方风险矩阵(对齐 service_system_config: risk.matrix.official,5×5 LS) */
|
||||||
|
export const DEFAULT_MATRIX: MatrixPayload = {
|
||||||
|
size: '5x5',
|
||||||
|
l_range: [1, 5],
|
||||||
|
s_range: [1, 5],
|
||||||
|
rr: [
|
||||||
|
{ min: 15, level: 'MAJOR', name: '重大' },
|
||||||
|
{ min: 10, level: 'HIGH', name: '较大' },
|
||||||
|
{ min: 5, level: 'MID', name: '一般' }
|
||||||
|
],
|
||||||
|
default_level: 'LOW'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** MOC 等级评分维度(对齐 change.level.dims,6 维固化) */
|
||||||
|
export const DEFAULT_LEVEL_DIMS: LevelDim[] = [
|
||||||
|
{ dim: 'EQUIPMENT', name: '设备影响', max: 4 },
|
||||||
|
{ dim: 'PROCESS', name: '工艺影响', max: 4 },
|
||||||
|
{ dim: 'SAFETY', name: '安全风险', max: 4 },
|
||||||
|
{ dim: 'ENVIRONMENT', name: '环境影响', max: 4 },
|
||||||
|
{ dim: 'QUALITY', name: '质量影响', max: 4 },
|
||||||
|
{ dim: 'SCHEDULE', name: '进度与成本', max: 4 }
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ---------- 变更智能分析(同步) ---------- */
|
||||||
|
|
||||||
|
export interface RecognizeResult {
|
||||||
|
rows: CompareRow[];
|
||||||
|
is_change: boolean;
|
||||||
|
same_kind_replace: boolean;
|
||||||
|
materials_suggest: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiRecognize = (content: string, deviceTags: string[] = []) =>
|
||||||
|
aiFetch<RecognizeResult>('/open/v1/moc/change/recognize', {
|
||||||
|
request_id: nextRequestId('rec'),
|
||||||
|
content,
|
||||||
|
device_tags: deviceTags
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface ApplicationResult {
|
||||||
|
name: string;
|
||||||
|
main_changes: string[];
|
||||||
|
related_changes: string[];
|
||||||
|
purposes: string[];
|
||||||
|
effect: string;
|
||||||
|
change_type: string;
|
||||||
|
level_suggestion: string | null;
|
||||||
|
duration_suggestion: string | null;
|
||||||
|
materials_text: string;
|
||||||
|
update_docs: string[];
|
||||||
|
disciplines: string[];
|
||||||
|
signers: Record<string, string> | null;
|
||||||
|
risk_tools: string[];
|
||||||
|
department_opinions?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiApplication = (content: string, rows: CompareRow[]) =>
|
||||||
|
aiFetch<ApplicationResult>('/open/v1/moc/change/application', {
|
||||||
|
request_id: nextRequestId('app'),
|
||||||
|
content,
|
||||||
|
rows
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface PreRiskResult {
|
||||||
|
severity: string;
|
||||||
|
probability: string;
|
||||||
|
protection: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiRiskPreAnalysis = (content: string, rows: CompareRow[]) =>
|
||||||
|
aiFetch<PreRiskResult>('/open/v1/moc/change/risk-pre-analysis', {
|
||||||
|
request_id: nextRequestId('pre'),
|
||||||
|
content,
|
||||||
|
rows
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface TypeJudgeResult {
|
||||||
|
type: string;
|
||||||
|
confidence: number;
|
||||||
|
conclusion: string;
|
||||||
|
hit_rules: string[];
|
||||||
|
exclude_rules: string[];
|
||||||
|
knowledge_refs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiTypeJudge = (content: string, rows: CompareRow[]) =>
|
||||||
|
aiFetch<TypeJudgeResult>('/open/v1/moc/change/type-judge', {
|
||||||
|
request_id: nextRequestId('type'),
|
||||||
|
content,
|
||||||
|
rows
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface LevelScoreResult {
|
||||||
|
dims: { dim: string; name: string; score: number | null; basis: string }[];
|
||||||
|
total_score: number;
|
||||||
|
analysis_text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiLevelScore = (content: string, rows: CompareRow[], levelDims: LevelDim[] = DEFAULT_LEVEL_DIMS) =>
|
||||||
|
aiFetch<LevelScoreResult>('/open/v1/moc/change/level-score', {
|
||||||
|
request_id: nextRequestId('level'),
|
||||||
|
content,
|
||||||
|
rows,
|
||||||
|
level_dims: levelDims
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface RiskCheckItem {
|
||||||
|
no?: number;
|
||||||
|
group: string;
|
||||||
|
q: string;
|
||||||
|
ans: string;
|
||||||
|
risk?: string;
|
||||||
|
measure?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiRiskCheck = (content: string, rows: CompareRow[], checkTemplate?: object) =>
|
||||||
|
aiFetch<{ items: RiskCheckItem[] }>('/open/v1/moc/change/risk-check', {
|
||||||
|
request_id: nextRequestId('check'),
|
||||||
|
content,
|
||||||
|
rows,
|
||||||
|
...(checkTemplate ? { check_template: checkTemplate } : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface RiskRecord {
|
||||||
|
item: string;
|
||||||
|
scene: string;
|
||||||
|
l: number | null;
|
||||||
|
s: number | null;
|
||||||
|
existing: string;
|
||||||
|
suggest: string;
|
||||||
|
residual_l: number | null;
|
||||||
|
residual_s: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiRiskOrganize = (content: string, rows: CompareRow[], toolResults?: object) =>
|
||||||
|
aiFetch<{ records: RiskRecord[] }>('/open/v1/moc/risk/organize', {
|
||||||
|
request_id: nextRequestId('org'),
|
||||||
|
content,
|
||||||
|
rows,
|
||||||
|
tool_results: toolResults ?? {},
|
||||||
|
matrix_payload: DEFAULT_MATRIX
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- 风险分析插件(同步,仅出 L/S) ---------- */
|
||||||
|
|
||||||
|
export interface HazopRow {
|
||||||
|
cause: string;
|
||||||
|
consequence: string;
|
||||||
|
l: number | null;
|
||||||
|
s: number | null;
|
||||||
|
safeguards: string;
|
||||||
|
suggestions: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiHazopDeviation = (payload: {
|
||||||
|
node_name: string;
|
||||||
|
deviation: string;
|
||||||
|
object_name?: string;
|
||||||
|
change_context?: object;
|
||||||
|
}) =>
|
||||||
|
aiFetch<{ rows: HazopRow[] }>('/open/v1/moc/hazop/deviation-analyze', {
|
||||||
|
request_id: nextRequestId('hazop'),
|
||||||
|
matrix_payload: DEFAULT_MATRIX,
|
||||||
|
...payload
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface JsaStep {
|
||||||
|
step: string;
|
||||||
|
desc: string;
|
||||||
|
rows: { hazard: string; consequence: string; l: number | null; s: number | null; safeguards: string; suggestions: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiJsaAnalyze = (payload: {
|
||||||
|
job_info: { job_name: string; job_type?: string; job_location?: string };
|
||||||
|
steps?: any[];
|
||||||
|
change_context?: object;
|
||||||
|
mode?: 'supplement' | 'full';
|
||||||
|
}) =>
|
||||||
|
aiFetch<{ steps: JsaStep[] }>('/open/v1/moc/jsa/analyze', {
|
||||||
|
request_id: nextRequestId('jsa'),
|
||||||
|
mode: 'supplement',
|
||||||
|
matrix_payload: DEFAULT_MATRIX,
|
||||||
|
...payload
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface SclGroup {
|
||||||
|
g: string;
|
||||||
|
rows: { item: string; std: string; consequence: string; l: number | null; s: number | null; controls: string; suggestions: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiSclAnalyze = (payload: {
|
||||||
|
device: { name: string; material_info?: string; run_params?: string };
|
||||||
|
category: string;
|
||||||
|
scl_template?: object;
|
||||||
|
change_context?: object;
|
||||||
|
}) =>
|
||||||
|
aiFetch<{ groups: SclGroup[] }>('/open/v1/moc/scl/analyze', {
|
||||||
|
request_id: nextRequestId('scl'),
|
||||||
|
matrix_payload: DEFAULT_MATRIX,
|
||||||
|
...payload
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- 辅助类 ---------- */
|
||||||
|
|
||||||
|
export const aiTraining = (content: string, rows: CompareRow[], riskRecords?: any[]) =>
|
||||||
|
aiFetch<{ training_text: string; suggest_posts: string[] }>('/open/v1/moc/change/training', {
|
||||||
|
request_id: nextRequestId('train'),
|
||||||
|
content,
|
||||||
|
rows,
|
||||||
|
risk_records: riskRecords ?? []
|
||||||
|
});
|
||||||
|
|
||||||
|
export const aiPssr = (payload: { content: string; change_type?: string; risk_records?: any[]; action_items?: any[] }) =>
|
||||||
|
aiFetch<{ groups: { g: string; items: { content: string; phase: string }[] }[] }>('/open/v1/moc/change/pssr', {
|
||||||
|
request_id: nextRequestId('pssr'),
|
||||||
|
...payload
|
||||||
|
});
|
||||||
|
|
||||||
|
export const aiAcceptance = (payload: { purpose?: string; effect?: string }) =>
|
||||||
|
aiFetch<{ rows: { item: string; basis: string; standard: string; method: string; owner_suggest: string }[] }>(
|
||||||
|
'/open/v1/moc/change/acceptance',
|
||||||
|
{ request_id: nextRequestId('acc'), ...payload }
|
||||||
|
);
|
||||||
|
|
||||||
|
export const aiDisposalDraft = (payload: { original_summary: string; disposal_type: string; long_term_scene?: string }) =>
|
||||||
|
aiFetch<{ reason: string; risk: string; long_term_risk_points: string[] }>('/open/v1/moc/disposal/draft', {
|
||||||
|
request_id: nextRequestId('disp'),
|
||||||
|
...payload
|
||||||
|
});
|
||||||
|
|
||||||
|
export const aiGlossary = () => aiFetch<{ terms: { word: string; explanation: string }[] }>('/open/v1/moc/glossary', undefined, 'GET');
|
||||||
|
|
||||||
|
/* ---------- 异步任务 ---------- */
|
||||||
|
|
||||||
|
export interface OpenTask {
|
||||||
|
task_id: string;
|
||||||
|
task_type?: string;
|
||||||
|
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||||
|
progress: number;
|
||||||
|
current_step?: string;
|
||||||
|
steps?: { step: string; status: string }[];
|
||||||
|
error_msg?: string;
|
||||||
|
result?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiApprovalAssist = (payload: {
|
||||||
|
snapshot: object;
|
||||||
|
change_id_ref?: string;
|
||||||
|
submit_version?: number;
|
||||||
|
inflight_changes?: any[];
|
||||||
|
manual_filled?: string[];
|
||||||
|
}) =>
|
||||||
|
aiFetch<{ task_id: string; status: string; estimated_seconds: number }>('/open/v1/moc/approval/assist', {
|
||||||
|
request_id: nextRequestId('assist'),
|
||||||
|
matrix_payload: DEFAULT_MATRIX,
|
||||||
|
level_dims: DEFAULT_LEVEL_DIMS,
|
||||||
|
...payload
|
||||||
|
});
|
||||||
|
|
||||||
|
export const aiKnowledgeIngest = (doc: {
|
||||||
|
name: string;
|
||||||
|
file_url?: string;
|
||||||
|
file_base64?: string;
|
||||||
|
version?: string;
|
||||||
|
category?: string;
|
||||||
|
hash?: string;
|
||||||
|
}) =>
|
||||||
|
aiFetch<{ task_id: string; status: string; estimated_seconds: number }>('/open/v1/moc/knowledge/ingest', {
|
||||||
|
request_id: nextRequestId('ingest'),
|
||||||
|
doc
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getTask = (taskId: string) => aiFetch<OpenTask>(`/open/v1/tasks/${taskId}`, undefined, 'GET');
|
||||||
|
|
||||||
|
export const getTaskResult = (taskId: string) => aiFetch<OpenTask>(`/open/v1/tasks/${taskId}/result`, undefined, 'GET');
|
||||||
|
|
||||||
|
/** 轮询任务直至完成/失败 */
|
||||||
|
export async function pollTask(
|
||||||
|
taskId: string,
|
||||||
|
opts: { intervalMs?: number; timeoutMs?: number; onProgress?: (t: OpenTask) => void } = {}
|
||||||
|
): Promise<any> {
|
||||||
|
const interval = opts.intervalMs ?? 3000;
|
||||||
|
const deadline = Date.now() + (opts.timeoutMs ?? 300_000);
|
||||||
|
for (;;) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const t = await getTask(taskId);
|
||||||
|
opts.onProgress?.(t);
|
||||||
|
if (t.status === 'completed') {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const r = await getTaskResult(taskId);
|
||||||
|
return r?.result ?? r;
|
||||||
|
}
|
||||||
|
if (t.status === 'failed') throw new AiError(50202, t.error_msg || 'AI 任务执行失败');
|
||||||
|
if (Date.now() > deadline) throw new AiError(50202, 'AI 任务等待超时,请稍后查看');
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await new Promise(r => setTimeout(r, interval));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 健康检查(联调验证用) */
|
||||||
|
export const aiHealth = () => aiFetch<{ status: string }>('/open/v1/health', undefined, 'GET');
|
||||||
@@ -3,6 +3,7 @@ import { useRoute } from 'vue-router';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useLoading } from '@sa/hooks';
|
import { useLoading } from '@sa/hooks';
|
||||||
import { fetchGetUserInfo, fetchLogin } from '@/service/api';
|
import { fetchGetUserInfo, fetchLogin } from '@/service/api';
|
||||||
|
import { setAiUser } from '@/service/api/ai';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
import { localStg } from '@/utils/storage';
|
import { localStg } from '@/utils/storage';
|
||||||
import { SetupStoreId } from '@/enum';
|
import { SetupStoreId } from '@/enum';
|
||||||
@@ -151,6 +152,13 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
|||||||
// update store
|
// update store
|
||||||
Object.assign(userInfo, info);
|
Object.assign(userInfo, info);
|
||||||
|
|
||||||
|
// 同步 AI 插件用户身份(exchange 换 ai_token 用)
|
||||||
|
setAiUser({
|
||||||
|
platform_user_id: String(info.userId),
|
||||||
|
align_key: info.username,
|
||||||
|
display_name: info.username
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
|
|||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { aiKnowledgeIngest, pollTask, AiError } from '@/service/api/ai';
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
@@ -29,8 +30,26 @@ const preview = (d:any) => {
|
|||||||
const download = (d:any) => {
|
const download = (d:any) => {
|
||||||
window.$message?.success(`演示:下载 ${d.name}`)
|
window.$message?.success(`演示:下载 ${d.name}`)
|
||||||
}
|
}
|
||||||
const call = (d:any) => {
|
// 已纳入 AI 知识库的条目(name -> true)与摄入中状态
|
||||||
window.$message?.success(`演示:已将「${d.name}」调用为当前变更的 AI 知识来源(调用记录留痕)`)
|
const ingested = ref<Record<string, boolean>>({});
|
||||||
|
const ingesting = ref<Record<string, boolean>>({});
|
||||||
|
const call = async (d:any) => {
|
||||||
|
if (ingesting.value[d.name] || ingested.value[d.name]) return;
|
||||||
|
ingesting.value[d.name] = true;
|
||||||
|
try {
|
||||||
|
const { task_id } = await aiKnowledgeIngest({ name: d.name, version: d.ver, category: d.cat });
|
||||||
|
await pollTask(task_id, {});
|
||||||
|
ingested.value[d.name] = true;
|
||||||
|
window.$message?.success(`已将「${d.name}」纳入 AI 知识库(调用记录留痕)`);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e instanceof AiError) {
|
||||||
|
window.$message?.warning(`AI 知识摄入失败:${e.message},可稍后重试`);
|
||||||
|
} else {
|
||||||
|
window.$message?.warning('AI 知识摄入失败,可稍后重试');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ingesting.value[d.name] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
interface LibDoc { name: string; ver: string; date: string }
|
interface LibDoc { name: string; ver: string; date: string }
|
||||||
interface LibFolder { label: string; docs: LibDoc[] }
|
interface LibFolder { label: string; docs: LibDoc[] }
|
||||||
@@ -210,7 +229,8 @@ const libCur = computed(() => LIB_ORGS.find((o) => o.org === libSel.value.org)?.
|
|||||||
<div class="flex shrink-0 gap-3 text-xs">
|
<div class="flex shrink-0 gap-3 text-xs">
|
||||||
<NButton type="primary" text class="hover:underline text-xs" @click="preview(d)">查看</NButton>
|
<NButton type="primary" text class="hover:underline text-xs" @click="preview(d)">查看</NButton>
|
||||||
<NButton type="primary" text class="hover:underline text-xs" @click="download(d)">下载</NButton>
|
<NButton type="primary" text class="hover:underline text-xs" @click="download(d)">下载</NButton>
|
||||||
<NButton type="primary" text class="hover:underline text-xs" @click="call(d)">调用</NButton>
|
<NTag v-if="ingested[d.name]" size="small" type="success" class="text-[10px]">已纳入 AI 知识库</NTag>
|
||||||
|
<NButton v-else type="primary" text class="hover:underline text-xs" :loading="!!ingesting[d.name]" @click="call(d)">调用</NButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed, watch } from 'vue';
|
||||||
|
import { aiApprovalAssist, pollTask, AiError } from '@/service/api/ai';
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { useRouterPush } from '@/hooks/common/router';
|
import { useRouterPush } from '@/hooks/common/router';
|
||||||
@@ -69,13 +70,14 @@ const confirmClose = () => {
|
|||||||
window.$message?.success('强制合规项已确认闭环,操作已留痕');
|
window.$message?.success('强制合规项已确认闭环,操作已留痕');
|
||||||
}
|
}
|
||||||
const conflictHide = ref<boolean>(false);
|
const conflictHide = ref<boolean>(false);
|
||||||
const PARAM_DIFF = [
|
// 关键参数对比(静态示例作 fallback,AI 审批辅助完成后回填)
|
||||||
|
const paramDiff = ref([
|
||||||
{ k: "反应温度", before: "80℃", after: "90℃", over: false, note: "仍在溶剂沸点与物料分解温度的安全裕度内" },
|
{ k: "反应温度", before: "80℃", after: "90℃", over: false, note: "仍在溶剂沸点与物料分解温度的安全裕度内" },
|
||||||
{ k: "保温时间", before: "8 h", after: "4 h", over: false, note: "终点判断窗口变窄,须修订操作规程" },
|
{ k: "保温时间", before: "8 h", after: "4 h", over: false, note: "终点判断窗口变窄,须修订操作规程" },
|
||||||
{ k: "DCS 温度高报警", before: "85℃", after: "95℃", over: true, note: "超出原报警裕度,须重新整定验证" },
|
{ k: "DCS 温度高报警", before: "85℃", after: "95℃", over: true, note: "超出原报警裕度,须重新整定验证" },
|
||||||
{ k: "联锁高高报", before: "90℃", after: "100℃", over: true, note: "接近设备设计温度上限,投用前须完成联锁测试" },
|
{ k: "联锁高高报", before: "90℃", after: "100℃", over: true, note: "接近设备设计温度上限,投用前须完成联锁测试" },
|
||||||
{ k: "未知杂质含量", before: "0.05%", after: "0.12%", over: false, note: "结构未定,须纳入日常分析计划" },
|
{ k: "未知杂质含量", before: "0.05%", after: "0.12%", over: false, note: "结构未定,须纳入日常分析计划" },
|
||||||
];
|
]);
|
||||||
const aiRead = ref<boolean>(false);
|
const aiRead = ref<boolean>(false);
|
||||||
const roleName = ref<string>('张工艺');
|
const roleName = ref<string>('张工艺');
|
||||||
const suggs = ref([
|
const suggs = ref([
|
||||||
@@ -152,6 +154,109 @@ const handleReminder = (m: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// AI 审批辅助(抽屉打开时触发,异步任务轮询;失败保留静态示例,不阻塞页面)
|
||||||
|
const aiAssistLoading = ref<boolean>(false);
|
||||||
|
const aiAssistStep = ref<string>('');
|
||||||
|
const aiAssistProgress = ref<number>(0);
|
||||||
|
const aiAssistLoaded = ref<boolean>(false);
|
||||||
|
// 强制合规描述(静态示例作 fallback)
|
||||||
|
const mandText = ref<string>('本次变更涉及 DCS 联锁设定值修改,依据 AQ/T 3034《化工企业工艺安全管理实施导则》,投用前须完成联锁测试并留存记录。');
|
||||||
|
const mandItem = ref<string>('联锁测试记录须在投用前完成并上传归档');
|
||||||
|
// 冲突提示
|
||||||
|
const conflictText = ref<string>('涉及设备 R-101 关联变更 MOC-2026-0038「机封冲洗方案调整」正在审批中,请确认施工时序与隔离方案无冲突。');
|
||||||
|
// 属地影响摘要
|
||||||
|
const deptRows = ref([
|
||||||
|
{ label: '涉及单元:', text: '一车间 R-201 反应釜及配套 DCS 回路,不影响相邻单元。' },
|
||||||
|
{ label: '现行规程影响:', text: '操作规程第 4.2 节(报警处置)需同步修订,变更关闭前完成受控换版。' },
|
||||||
|
{ label: '交接班提示:', text: '投用后首个班次需向班组交代新报警值与处置卡位置,建议纳入班前会内容。' },
|
||||||
|
]);
|
||||||
|
// 参考信息(相似变更 / 历史经验)
|
||||||
|
const similarText = ref<string>('相似变更 MOC-2025-0112「酯化反应温度上调」投用后运行正常,无异常记录');
|
||||||
|
const experienceText = ref<string>('历史经验:温度类变更投用后首周超温报警占比约 38%,建议关注投用首周报警趋势');
|
||||||
|
|
||||||
|
const toTextList = (v: any): string[] => {
|
||||||
|
if (!v) return [];
|
||||||
|
if (typeof v === 'string') return [v];
|
||||||
|
if (Array.isArray(v)) return v.map((x: any) => (typeof x === 'string' ? x : x?.text ?? x?.content ?? '')).filter(Boolean);
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const runAiAssist = async () => {
|
||||||
|
if (aiAssistLoading.value || aiAssistLoaded.value) return;
|
||||||
|
aiAssistLoading.value = true;
|
||||||
|
aiAssistStep.value = '提交分析任务…';
|
||||||
|
aiAssistProgress.value = 0;
|
||||||
|
try {
|
||||||
|
const snapshot = {
|
||||||
|
apply_form: currentInfo.value ?? {},
|
||||||
|
compare_rows: [],
|
||||||
|
risk_records: [],
|
||||||
|
materials: []
|
||||||
|
};
|
||||||
|
const { task_id } = await aiApprovalAssist({
|
||||||
|
snapshot,
|
||||||
|
change_id_ref: currentInfo.value?.cid,
|
||||||
|
submit_version: 1
|
||||||
|
});
|
||||||
|
const result: any = await pollTask(task_id, {
|
||||||
|
onProgress: t => {
|
||||||
|
aiAssistStep.value = t.current_step || 'AI 分析中…';
|
||||||
|
aiAssistProgress.value = t.progress ?? 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const r = result?.result ?? result ?? {};
|
||||||
|
// 强制合规(mandatory 可能为字符串或数组)
|
||||||
|
const mand = toTextList(r.mandatory);
|
||||||
|
if (mand.length) {
|
||||||
|
mandText.value = mand.join(';');
|
||||||
|
mandItem.value = mand[0];
|
||||||
|
}
|
||||||
|
// 冲突提示
|
||||||
|
const conflicts = toTextList(r.conflicts);
|
||||||
|
if (conflicts.length) conflictText.value = conflicts.join(';');
|
||||||
|
// 关键参数对比
|
||||||
|
if (Array.isArray(r.param_diff) && r.param_diff.length) {
|
||||||
|
paramDiff.value = r.param_diff.map((d: any) => ({
|
||||||
|
k: d.k ?? d.item ?? d.name ?? '',
|
||||||
|
before: d.before ?? '',
|
||||||
|
after: d.after ?? '',
|
||||||
|
over: Boolean(d.over),
|
||||||
|
note: d.note ?? d.basis ?? ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// 推荐改进项
|
||||||
|
const sugs = toTextList(r.suggestions);
|
||||||
|
if (sugs.length) suggs.value = sugs.map((text, i) => ({ id: i + 1, text, st: '待定' }));
|
||||||
|
// 相似变更 / 历史经验
|
||||||
|
const similar = toTextList(r.similar_changes);
|
||||||
|
if (similar.length) similarText.value = similar[0];
|
||||||
|
const exp = toTextList(r.experience);
|
||||||
|
if (exp.length) experienceText.value = exp[0];
|
||||||
|
// 属地影响摘要(字符串或数组,兼容对象形态)
|
||||||
|
if (r.dept_summary) {
|
||||||
|
const rows = toTextList(r.dept_summary).map((t: string) => {
|
||||||
|
const idx = t.indexOf(':');
|
||||||
|
return idx > 0 ? { label: `${t.slice(0, idx + 1)}`, text: t.slice(idx + 1) } : { label: '', text: t };
|
||||||
|
});
|
||||||
|
if (rows.length) deptRows.value = rows;
|
||||||
|
}
|
||||||
|
aiAssistLoaded.value = true;
|
||||||
|
window.$message?.success('AI 审批辅助分析完成,结果仅供参考,需人工确认');
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e instanceof AiError) {
|
||||||
|
window.$message?.warning(`AI 分析失败:${e.message},展示静态示例,可人工判断`);
|
||||||
|
} else {
|
||||||
|
window.$message?.warning('AI 分析失败,展示静态示例,可人工判断');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
aiAssistLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(aiDrawer, v => {
|
||||||
|
if (v) runAiAssist();
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if(route.query && route.query.params){
|
if(route.query && route.query.params){
|
||||||
currentInfo.value = JSON.parse(route.query.params as string);
|
currentInfo.value = JSON.parse(route.query.params as string);
|
||||||
@@ -1038,6 +1143,14 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
<!-- AI 分析进度(异步任务轮询) -->
|
||||||
|
<div v-if="aiAssistLoading" class="rounded-lg border border-violet-200 bg-violet-50 px-3 py-2.5 text-xs text-violet-700">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Icon icon="eos-icons:loading" class="size-14px" />
|
||||||
|
<span>{{ aiAssistStep || 'AI 分析中…' }}({{ aiAssistProgress }}%)</span>
|
||||||
|
</div>
|
||||||
|
<NProgress type="line" :percentage="aiAssistProgress" :show-indicator="false" class="mt-1.5" processing />
|
||||||
|
</div>
|
||||||
<!-- 合规警告条(强制合规项,不可关闭;闭环后转绿) -->
|
<!-- 合规警告条(强制合规项,不可关闭;闭环后转绿) -->
|
||||||
<div
|
<div
|
||||||
class="px-3 py-2.5 text-sm"
|
class="px-3 py-2.5 text-sm"
|
||||||
@@ -1064,7 +1177,7 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs leading-relaxed">
|
<p class="mt-1 text-xs leading-relaxed">
|
||||||
本次变更涉及 DCS 联锁设定值修改,依据 AQ/T 3034《化工企业工艺安全管理实施导则》,投用前须完成联锁测试并留存记录。
|
{{ mandText }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="!mandClosed" class="mt-1 text-right">
|
<div v-if="!mandClosed" class="mt-1 text-right">
|
||||||
<NButton text type="primary" class="text-xs" @click="confirmClose">确认闭环(留痕)</NButton>
|
<NButton text type="primary" class="text-xs" @click="confirmClose">确认闭环(留痕)</NButton>
|
||||||
@@ -1086,7 +1199,7 @@ onMounted(() => {
|
|||||||
<NButton text type="primary" class="ml-auto shrink-0 text-xs" @click="conflictHide = true">知道了</NButton>
|
<NButton text type="primary" class="ml-auto shrink-0 text-xs" @click="conflictHide = true">知道了</NButton>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs leading-relaxed">
|
<p class="mt-1 text-xs leading-relaxed">
|
||||||
涉及设备 R-101 关联变更 <b>MOC-2026-0038「机封冲洗方案调整」</b>正在审批中,请确认施工时序与隔离方案无冲突。
|
{{ conflictText }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- 属地影响摘要(仅属地负责人视角) -->
|
<!-- 属地影响摘要(仅属地负责人视角) -->
|
||||||
@@ -1099,9 +1212,7 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5 px-3 py-2.5 text-xs leading-relaxed text-slate-600">
|
<div class="space-y-1.5 px-3 py-2.5 text-xs leading-relaxed text-slate-600">
|
||||||
<p><b class="text-slate-700">涉及单元:</b>一车间 R-201 反应釜及配套 DCS 回路,不影响相邻单元。</p>
|
<p v-for="(r, i) in deptRows" :key="i"><b class="text-slate-700">{{ r.label }}</b>{{ r.text }}</p>
|
||||||
<p><b class="text-slate-700">现行规程影响:</b>操作规程第 4.2 节(报警处置)需同步修订,变更关闭前完成受控换版。</p>
|
|
||||||
<p><b class="text-slate-700">交接班提示:</b>投用后首个班次需向班组交代新报警值与处置卡位置,建议纳入班前会内容。</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 变更前后关键参数对比(变化标蓝 / 超限标红) -->
|
<!-- 变更前后关键参数对比(变化标蓝 / 超限标红) -->
|
||||||
@@ -1116,7 +1227,7 @@ onMounted(() => {
|
|||||||
<div class="border-b bg-slate-50/70 px-3 py-2 text-xs font-semibold text-slate-600">
|
<div class="border-b bg-slate-50/70 px-3 py-2 text-xs font-semibold text-slate-600">
|
||||||
关键参数对比(AI 提取) · <span class="font-bold text-[var(--theme-color)]">蓝=变化</span> · <span class="font-bold text-[var(--error-color)]">红=超限</span>
|
关键参数对比(AI 提取) · <span class="font-bold text-[var(--theme-color)]">蓝=变化</span> · <span class="font-bold text-[var(--error-color)]">红=超限</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="d in PARAM_DIFF" :key="d.k" class="border-b px-3 py-2 last:border-b-0">
|
<div v-for="d in paramDiff" :key="d.k" class="border-b px-3 py-2 last:border-b-0">
|
||||||
<div class="flex flex-wrap items-baseline gap-x-2 text-sm">
|
<div class="flex flex-wrap items-baseline gap-x-2 text-sm">
|
||||||
<span class="w-24 shrink-0 text-xs text-slate-500">{{ d.k }}</span>
|
<span class="w-24 shrink-0 text-xs text-slate-500">{{ d.k }}</span>
|
||||||
<span class="text-xs text-slate-400">{{ d.before }}</span>
|
<span class="text-xs text-slate-400">{{ d.before }}</span>
|
||||||
@@ -1159,7 +1270,7 @@ onMounted(() => {
|
|||||||
<!-- 强制合规项 -->
|
<!-- 强制合规项 -->
|
||||||
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--error-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--error-color)]" /> 强制合规项(驳回前必须处理,否则无法通过)</div>
|
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--error-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--error-color)]" /> 强制合规项(驳回前必须处理,否则无法通过)</div>
|
||||||
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-red-100 bg-red-50/50 px-3 py-2 text-sm">
|
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-red-100 bg-red-50/50 px-3 py-2 text-sm">
|
||||||
<span class="min-w-0 flex-1 text-slate-700">联锁测试记录须在投用前完成并上传归档</span>
|
<span class="min-w-0 flex-1 text-slate-700">{{ mandItem }}</span>
|
||||||
<NTag v-if="mandClosed" size="small" type="success">已闭环</NTag>
|
<NTag v-if="mandClosed" size="small" type="success">已闭环</NTag>
|
||||||
<NTag v-else size="small" type="warning">未闭环</NTag>
|
<NTag v-else size="small" type="warning">未闭环</NTag>
|
||||||
</div>
|
</div>
|
||||||
@@ -1191,11 +1302,11 @@ onMounted(() => {
|
|||||||
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--theme-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--theme-color)]" /> 参考信息(纯参考,无强制力)</div>
|
<div class="mb-1 flex items-center gap-1.5 text-xs font-semibold text-[var(--theme-color)]"><span class="inline-block h-2 w-2 rounded-full bg-[var(--theme-color)]" /> 参考信息(纯参考,无强制力)</div>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm">
|
<div class="flex flex-wrap items-center gap-2 rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm">
|
||||||
<span class="min-w-0 flex-1 text-slate-700">相似变更 <b>MOC-2025-0112「酯化反应温度上调」</b>投用后运行正常,无异常记录</span>
|
<span class="min-w-0 flex-1 text-slate-700">{{ similarText }}</span>
|
||||||
<NButton text type="primary" class="hover:underline text-xs" @click="viewArchive('MOC-2025-0112')">查看归档包</NButton>
|
<NButton text type="primary" class="hover:underline text-xs" @click="viewArchive('MOC-2025-0112')">查看归档包</NButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm text-slate-700">
|
<div class="rounded-lg border border-[#DFE9F6] bg-[#EAF0F9]/60 px-3 py-2 text-sm text-slate-700">
|
||||||
历史经验:温度类变更投用后首周超温报警占比约 38%,建议关注投用首周报警趋势
|
{{ experienceText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ import {
|
|||||||
CHANGES, type ChangeLevel, type ChangeOrder, type ChangeStatus, type ChangeType,
|
CHANGES, type ChangeLevel, type ChangeOrder, type ChangeStatus, type ChangeType,
|
||||||
} from "@/typings/model";
|
} from "@/typings/model";
|
||||||
import { getColorWithOpacity } from "@/utils/common";
|
import { getColorWithOpacity } from "@/utils/common";
|
||||||
|
import {
|
||||||
|
AiError,
|
||||||
|
aiApplication,
|
||||||
|
aiLevelScore,
|
||||||
|
aiRecognize,
|
||||||
|
aiRiskOrganize,
|
||||||
|
aiRiskPreAnalysis,
|
||||||
|
aiTypeJudge,
|
||||||
|
type CompareRow,
|
||||||
|
} from "@/service/api/ai";
|
||||||
import HighlightText from "./modules/HighlightText.vue";
|
import HighlightText from "./modules/HighlightText.vue";
|
||||||
import Ed from "./modules/Ed.vue";
|
import Ed from "./modules/Ed.vue";
|
||||||
import Hazop from "./modules/hazop.vue";
|
import Hazop from "./modules/hazop.vue";
|
||||||
@@ -73,16 +83,28 @@ const AI_ROWS = [
|
|||||||
},
|
},
|
||||||
{ id: 4, item: "操作方式", before: "DCS自动化控制,远程操作,单人监控。", after: "无变化" },
|
{ id: 4, item: "操作方式", before: "DCS自动化控制,远程操作,单人监控。", after: "无变化" },
|
||||||
];
|
];
|
||||||
|
// AI 调用失败统一提示:不阻塞页面、不清空已有数据,保留手动编辑能力(P1)
|
||||||
|
const aiFail = (e: any) => {
|
||||||
|
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
|
||||||
|
else window.$message?.warning("AI 分析失败,可手动填写");
|
||||||
|
};
|
||||||
|
// 页面当前对比行 → AI 契约行
|
||||||
|
const compareRows = (): CompareRow[] => (rows.value ?? []).map((x: any) => ({ item: x.item, before: x.before, after: x.after, hl: x.hl }));
|
||||||
// ---------- AI 变更识别 ----------
|
// ---------- AI 变更识别 ----------
|
||||||
const runIdentify = () => {
|
const runIdentify = async () => {
|
||||||
if (!desc.value.trim()) { window.$message?.warning("请先输入变更内容描述"); return; }
|
if (!desc.value.trim()) { window.$message?.warning("请先输入变更内容描述"); return; }
|
||||||
analyzing.value = true;
|
analyzing.value = true;
|
||||||
rows.value = null;
|
rows.value = null;
|
||||||
setTimeout(() => {
|
try {
|
||||||
|
const res = await aiRecognize(desc.value);
|
||||||
|
rows.value = (res?.rows ?? []).map((x, i) => ({ id: i + 1, item: x.item, before: x.before, after: x.after, hl: x.hl }));
|
||||||
|
window.$message?.success(`AI 变更识别完成,共 ${rows.value.length} 项,请逐项人工确认(可直接修改)`);
|
||||||
|
} catch (e: any) {
|
||||||
rows.value = AI_ROWS.map((x) => ({ ...x }));
|
rows.value = AI_ROWS.map((x) => ({ ...x }));
|
||||||
|
aiFail(e);
|
||||||
|
} finally {
|
||||||
analyzing.value = false;
|
analyzing.value = false;
|
||||||
window.$message?.success("AI 变更识别完成,共 4 项,请逐项人工确认(可直接修改)");
|
}
|
||||||
}, 1200);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const editing = ref<any>(null);
|
const editing = ref<any>(null);
|
||||||
@@ -95,13 +117,23 @@ const preRiskText = ref({
|
|||||||
probability: "变更引入了新的偏差场景。首先,反应温度更接近溶剂回流温度,温控失效或冷却水中断时,超温溜温概率上升,可能引发冲料;其次,保温时间缩短使反应终点判断窗口变窄,人为误判导致转化率不足或副反应增加的概率上升;再者,未知杂质含量升高(0.05%→0.12%),其热稳定性尚未定性,长期累积可能加速釜壁结垢与搅拌负荷异常;此外,DCS报警/联锁值上调后原有报警裕度改变,误报、漏报概率需重新评估。",
|
probability: "变更引入了新的偏差场景。首先,反应温度更接近溶剂回流温度,温控失效或冷却水中断时,超温溜温概率上升,可能引发冲料;其次,保温时间缩短使反应终点判断窗口变窄,人为误判导致转化率不足或副反应增加的概率上升;再者,未知杂质含量升高(0.05%→0.12%),其热稳定性尚未定性,长期累积可能加速釜壁结垢与搅拌负荷异常;此外,DCS报警/联锁值上调后原有报警裕度改变,误报、漏报概率需重新评估。",
|
||||||
protection: "变更主要影响与温度控制相关的保护层。原DCS温度高报警85℃、高高报警及联锁90℃已不适用于新工艺窗口,需同步上调至95℃/100℃并重新整定验证;安全阀起跳压力与泄放能力不受本次温度调整直接影响,仍然有效;操作规程与应急预案须修订后,方可继续作为有效的人为保护层。建议投用前完成联锁测试与报警验证,并将杂质含量纳入日常分析计划。",
|
protection: "变更主要影响与温度控制相关的保护层。原DCS温度高报警85℃、高高报警及联锁90℃已不适用于新工艺窗口,需同步上调至95℃/100℃并重新整定验证;安全阀起跳压力与泄放能力不受本次温度调整直接影响,仍然有效;操作规程与应急预案须修订后,方可继续作为有效的人为保护层。建议投用前完成联锁测试与报警验证,并将杂质含量纳入日常分析计划。",
|
||||||
});
|
});
|
||||||
const runRiskAnalysis = () => {
|
const runRiskAnalysis = async () => {
|
||||||
analyzing.value = true;
|
analyzing.value = true;
|
||||||
setTimeout(() => {
|
try {
|
||||||
analyzing.value = false;
|
const res = await aiRiskPreAnalysis(desc.value, compareRows());
|
||||||
|
preRiskText.value = {
|
||||||
|
severity: res?.severity ?? preRiskText.value.severity,
|
||||||
|
probability: res?.probability ?? preRiskText.value.probability,
|
||||||
|
protection: res?.protection ?? preRiskText.value.protection,
|
||||||
|
};
|
||||||
preRiskShown.value = true;
|
preRiskShown.value = true;
|
||||||
window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
|
window.$message?.success("AI 风险预分析报告已生成(危害严重性 / 事件概率 / 保护措施影响),内容支持手动修改");
|
||||||
}, 1300);
|
} catch (e: any) {
|
||||||
|
preRiskShown.value = true;
|
||||||
|
aiFail(e);
|
||||||
|
} finally {
|
||||||
|
analyzing.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const saveDraft = () => window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
const saveDraft = () => window.$message?.success("已保存为草稿,可在左侧「我的草稿」中继续编辑");
|
||||||
|
|
||||||
@@ -111,38 +143,48 @@ const materialsText = ref("");
|
|||||||
const updateDocs = ref<string[]>([]);
|
const updateDocs = ref<string[]>([]);
|
||||||
const disciplines = ref<string[]>([]);
|
const disciplines = ref<string[]>([]);
|
||||||
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
|
const tools = ref<string[]>(["HAZOP","JSA", "SCL", "RISK_CHECK"]);
|
||||||
const genForm = () => {
|
const UPDATE_DOC_OPTIONS = ['PID图纸', '操作规程', '总图', '设备台账', '工艺卡片', '联锁台账', '应急预案'];
|
||||||
|
const DISCIPLINE_OPTIONS = ['技术', '仪表', '电气', '环保', '质量'];
|
||||||
|
const genForm = async () => {
|
||||||
if (formGen.value === "generating") return;
|
if (formGen.value === "generating") return;
|
||||||
formGen.value = "generating";
|
formGen.value = "generating";
|
||||||
window.$message?.info("AI 正在生成变更申请单…(「变更申请表」标签已显示生成中标识)");
|
window.$message?.info("AI 正在生成变更申请单…(「变更申请表」标签已显示生成中标识)");
|
||||||
setTimeout(() => {
|
try {
|
||||||
const main: string[] = [];
|
const res = await aiApplication(desc.value, compareRows());
|
||||||
for (const row of rows.value ?? []) {
|
const pick = (v: string | null | undefined, opts: string[], cur: string) => (v && opts.includes(v) ? v : cur);
|
||||||
if (row.after && row.after !== "无变化" && row.item === "运行条件") {
|
const main = (res?.main_changes ?? []).filter((x) => x && x.trim());
|
||||||
main.push("缩合反应温度从80±2℃提升至90±2℃");
|
const purposes = (res?.purposes ?? []).filter((x) => PURPOSE_OPTIONS.includes(x));
|
||||||
main.push("保温时间从8小时缩短至4小时");
|
const docs = (res?.update_docs ?? []).filter((x) => UPDATE_DOC_OPTIONS.includes(x));
|
||||||
main.push("DCS温度高报警从85℃上调至95℃,高高报警及联锁值从90℃上调至100℃");
|
const discs = (res?.disciplines ?? []).filter((x) => DISCIPLINE_OPTIONS.includes(x));
|
||||||
}
|
const aiTools = (res?.risk_tools ?? [])
|
||||||
}
|
.map((x) => RISK_TOOLS_FORM.find((t) => t.key === x || t.label === x)?.key)
|
||||||
|
.filter((x): x is string => !!x);
|
||||||
form.value = {
|
form.value = {
|
||||||
name: "缩合反应温度及时间优化",
|
...form.value,
|
||||||
dept: "",
|
name: res?.name || form.value.name,
|
||||||
mainChanges: main.length ? main : ["(请根据识别结果填写主要变更)"],
|
mainChanges: main.length ? main : form.value.mainChanges,
|
||||||
related: "更新操作规程、DCS联锁设定值、工艺安全信息、岗位操作法",
|
related: (res?.related_changes ?? []).filter(Boolean).join("、") || form.value.related,
|
||||||
purposes: ["提高生产效能", "改善操作条件"],
|
purposes: purposes.length ? purposes : form.value.purposes,
|
||||||
effect: "在保持转化率不低于98%的前提下,反应时间缩短50%,产能提升一倍",
|
effect: res?.effect || form.value.effect,
|
||||||
type: "工艺", level: "一般", duration: "永久", urgent: false, date: "", restoreDate: "",
|
type: pick(res?.change_type, ['工艺', '设备', '管理'], form.value.type),
|
||||||
|
duration: pick(res?.duration_suggestion, ['永久', '临时'], form.value.duration),
|
||||||
};
|
};
|
||||||
materialsText.value = "1. 施工方案(含作业步骤、隔离与置换方案、作业许可)\n2. 保温材料(釜体保温更换,硅酸铝纤维毡,厚度按设计核算)\n3. 紧固件与垫片(法兰螺栓、金属缠绕垫,材质与原设计一致)\n4. 防腐涂料(釜体外表面除锈防腐)\n5. 脚手架搭拆材料及验收记录";
|
if (res?.materials_text) materialsText.value = res.materials_text;
|
||||||
updateDocs.value = ["PID图纸", "操作规程", "总图", "设备台账"];
|
if (docs.length) updateDocs.value = docs;
|
||||||
disciplines.value = ["技术", "仪表"];
|
if (discs.length) {
|
||||||
signers.value = { 技术: "王仪表(技术部)", 仪表: "王仪表(技术部 · 仪表)" };
|
disciplines.value = discs;
|
||||||
tools.value = ["HAZOP","JSA", "SCL", "通用检查表"];
|
toggleDiscipline(discs);
|
||||||
|
}
|
||||||
|
if (res?.signers && Object.keys(res.signers).length) signers.value = { ...res.signers };
|
||||||
|
if (aiTools.length) tools.value = aiTools;
|
||||||
currentPlugin.value = "";
|
currentPlugin.value = "";
|
||||||
formGen.value = "done";
|
formGen.value = "done";
|
||||||
tab.value = "form";
|
tab.value = "form";
|
||||||
window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改");
|
window.$message?.success("变更申请单已生成:AI 自动总结名称、预填内容,均可手动修改");
|
||||||
}, 1400);
|
} catch (e: any) {
|
||||||
|
formGen.value = "idle";
|
||||||
|
aiFail(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// ---------- 审批流程设置 ----------
|
// ---------- 审批流程设置 ----------
|
||||||
const approvers = ref<Record<string, string>>({
|
const approvers = ref<Record<string, string>>({
|
||||||
@@ -442,16 +484,85 @@ const toPssrAll = () => {
|
|||||||
riskRecords.value = riskRecords.value.map((x:any) => ({ ...x, pssr: true }));
|
riskRecords.value = riskRecords.value.map((x:any) => ({ ...x, pssr: true }));
|
||||||
window.$message?.success(`已将 ${rest.length} 条建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」)`);
|
window.$message?.success(`已将 ${rest.length} 条建议措施全部转为 PSSR 行动项(见「PSSR 检查内容」)`);
|
||||||
};
|
};
|
||||||
const aiOrganize = () => {
|
// L/S → 页面风险等级(高/中/低)换算,对齐官方 5×5 矩阵阈值
|
||||||
const gen: any[] = [
|
const lsLevel = (l: number | null, s: number | null) => {
|
||||||
{ item: "超温溜温冲料", scene: "反应温度提升至 90℃ 后更接近溶剂回流温度,温控失效或冷却水中断时反应溜温,釜压骤升可能导致冲料与物料喷溅", inherent: "高", existing: "DCS 温度高报 95℃、高高报及联锁 100℃(随变更上调并重新整定);紧急冷却系统有效", suggest: "1. 投用前完成联锁传动测试并留存记录;2. 投用首周每班复核温度趋势,纳入交接班巡检", residual: "中", source: "AI 整理" },
|
const rr = (l ?? 0) * (s ?? 0);
|
||||||
{ item: "未知杂质累积影响", scene: "未知杂质含量由 0.05% 升至 0.12%,热稳定性尚未定性,长期累积加速釜壁结垢与搅拌负荷异常", inherent: "中", existing: "小试热稳定性筛查;安全阀泄放能力不受本次调整影响", suggest: "1. 将杂质含量纳入日常分析计划(每周 1 次,连续 8 周);2. 跟踪结垢趋势,安排周期清洗", residual: "低", source: "AI 整理" },
|
return rr >= 10 ? "高" : rr >= 5 ? "中" : "低";
|
||||||
{ item: "联锁 / 报警设定失效", scene: "报警 / 联锁值上调后(高报 85→95℃、联锁 90→100℃),原有报警裕度改变,存在误报、漏报风险", inherent: "中", existing: "参数修改执行申请-复核双人确认", suggest: "1. 联锁逻辑图与台账同步更新并归档;2. 联锁测试纳入 PSSR 必查项", residual: "低", source: "AI 整理" },
|
};
|
||||||
];
|
const aiOrganize = async () => {
|
||||||
|
analyzing.value = true;
|
||||||
|
try {
|
||||||
|
const res = await aiRiskOrganize(desc.value, compareRows());
|
||||||
|
const gen = (res?.records ?? []).map((r) => ({
|
||||||
|
item: r.item,
|
||||||
|
scene: r.scene,
|
||||||
|
inherent: lsLevel(r.l, r.s),
|
||||||
|
existing: r.existing,
|
||||||
|
suggest: r.suggest,
|
||||||
|
residual: lsLevel(r.residual_l, r.residual_s),
|
||||||
|
source: "AI 整理",
|
||||||
|
}));
|
||||||
riskRecords.value = [...riskRecords.value, ...gen];
|
riskRecords.value = [...riskRecords.value, ...gen];
|
||||||
window.$message?.success("AI 已根据变更内容自动整理形成风险分析记录表(3 项),支持手动修改");
|
window.$message?.success(`AI 已根据变更内容自动整理形成风险分析记录表(${gen.length} 项),支持手动修改`);
|
||||||
|
} catch (e: any) {
|
||||||
|
aiFail(e);
|
||||||
|
} finally {
|
||||||
|
analyzing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// ---------- AI 类型判定 / 等级评分(抽屉打开时调用,静态示例作 fallback) ----------
|
||||||
|
const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_rules: string[]; exclude_rules: string[]; knowledge_refs: string[] } | null>(null);
|
||||||
|
const typeAiLoading = ref<boolean>(false);
|
||||||
|
const openTypeSheet = async () => {
|
||||||
|
sheet.value = "type";
|
||||||
|
if (typeAiLoading.value) return;
|
||||||
|
typeAiLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await aiTypeJudge(desc.value, compareRows());
|
||||||
|
typeAi.value = {
|
||||||
|
type: res?.type ?? "",
|
||||||
|
confidence: res?.confidence ?? 0,
|
||||||
|
conclusion: res?.conclusion ?? "",
|
||||||
|
hit_rules: res?.hit_rules ?? [],
|
||||||
|
exclude_rules: res?.exclude_rules ?? [],
|
||||||
|
knowledge_refs: res?.knowledge_refs ?? [],
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
aiFail(e);
|
||||||
|
} finally {
|
||||||
|
typeAiLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const levelAiText = ref<string>("");
|
||||||
|
const levelAiBasis = ref<string[]>([]);
|
||||||
|
const levelAiLoading = ref<boolean>(false);
|
||||||
|
const openLevelSheet = async () => {
|
||||||
|
sheet.value = "level";
|
||||||
|
if (levelAiLoading.value) return;
|
||||||
|
levelAiLoading.value = true;
|
||||||
|
try {
|
||||||
|
const dims = LEVEL_DIMS.map((d, i) => ({ dim: `DIM_${i + 1}`, name: d.name, max: Math.max(...d.options.map((o) => o.score)) }));
|
||||||
|
const res = await aiLevelScore(desc.value, compareRows(), dims);
|
||||||
|
// 返回维度按 name 匹配、匹配不到按顺序对齐回填各维度得分;总分/等级阈值换算沿用页面本地逻辑
|
||||||
|
const scores = [...levelScores.value];
|
||||||
|
(res?.dims ?? []).forEach((d, i) => {
|
||||||
|
const di = LEVEL_DIMS.findIndex((x) => x.name === d.name || x.name.endsWith(d.name));
|
||||||
|
const idx = di >= 0 ? di : i;
|
||||||
|
if (idx >= 0 && idx < scores.length && typeof d.score === "number") scores[idx] = d.score;
|
||||||
|
if (d.basis) levelAiBasis.value[idx] = d.basis;
|
||||||
|
});
|
||||||
|
levelScores.value = scores;
|
||||||
|
levelAiText.value = res?.analysis_text ?? "";
|
||||||
|
} catch (e: any) {
|
||||||
|
aiFail(e);
|
||||||
|
} finally {
|
||||||
|
levelAiLoading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// ---------- 变更培训 ----------
|
// ---------- 变更培训 ----------
|
||||||
|
// 注:培训区块暂无独立「AI 生成」触发按钮,保留静态示例内容;
|
||||||
|
// 接入时可在此调 aiTraining(desc.value, compareRows(), riskRecords.value),
|
||||||
|
// 将 training_text 填入 trainText、suggest_posts 展示为建议培训岗位。
|
||||||
const trainText = ref(
|
const trainText = ref(
|
||||||
`培训对象:R-201 岗位操作人员(4 人)、维修班(2 人)
|
`培训对象:R-201 岗位操作人员(4 人)、维修班(2 人)
|
||||||
培训方式:现场讲解 + 操作演示
|
培训方式:现场讲解 + 操作演示
|
||||||
@@ -470,6 +581,9 @@ M-201A 搅拌电机由 55kW 更换为 75kW 防爆电机(ExdⅡBT4 → ExdⅡCT
|
|||||||
3. 风险:防爆接合面或格兰头密封失效 → 应对:检查防爆接合面与格兰头,严禁带电开盖`
|
3. 风险:防爆接合面或格兰头密封失效 → 应对:检查防爆接合面与格兰头,严禁带电开盖`
|
||||||
);
|
);
|
||||||
// ---------- PSSR 检查内容 ----------
|
// ---------- PSSR 检查内容 ----------
|
||||||
|
// 注:PSSR 区块暂无独立「AI 生成」触发按钮,保留静态示例清单;
|
||||||
|
// 接入时可在此调 aiPssr({ content: desc.value, change_type: form.value.type, risk_records: riskRecords.value }),
|
||||||
|
// 将 groups(g, items: content/phase) 映射为 pssrGroups 的 { g, items: { c, p } } 结构。
|
||||||
const pssrDone = ref<Record<string, { by: string; at: string }>>({});
|
const pssrDone = ref<Record<string, { by: string; at: string }>>({});
|
||||||
const pssrNa = ref<Record<string, boolean>>({});
|
const pssrNa = ref<Record<string, boolean>>({});
|
||||||
const pssrActive = computed(() => pssrGroups.value.filter((g:any) => !pssrNa.value[g.g]));
|
const pssrActive = computed(() => pssrGroups.value.filter((g:any) => !pssrNa.value[g.g]));
|
||||||
@@ -507,6 +621,9 @@ const onPssrFile = (e: Event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ---------- 验收评价 ----------
|
// ---------- 验收评价 ----------
|
||||||
|
// 注:验收清单暂无独立「AI 生成」触发按钮,保留静态示例行;
|
||||||
|
// 接入时可在此调 aiAcceptance({ purpose: form.value.purposes.join('、'), effect: form.value.effect }),
|
||||||
|
// 将 rows(item/basis/standard/method/owner_suggest) 映射为 acceptRows 行结构(id 自行补,result 默认「待验收」)。
|
||||||
const acceptRows = ref([
|
const acceptRows = ref([
|
||||||
{ id: 1, item: "反应转化率", basis: "转化率不低于 98%", standard: "转化率 ≥ 98%(连续 3 批化验合格)", method: "化验室逐批取样分析,出具检验报告", owner: "车间 · 张工艺", result: "待验收" },
|
{ id: 1, item: "反应转化率", basis: "转化率不低于 98%", standard: "转化率 ≥ 98%(连续 3 批化验合格)", method: "化验室逐批取样分析,出具检验报告", owner: "车间 · 张工艺", result: "待验收" },
|
||||||
{ id: 2, item: "单批反应时间 / 产能", basis: "反应时间缩短 50%、产能提升一倍", standard: "保温时间 4h±0.5h,产能提升 ≥ 80%", method: "DCS 批次记录统计,对比变更前 30 天均值", owner: "车间 · 李主任", result: "待验收" },
|
{ id: 2, item: "单批反应时间 / 产能", basis: "反应时间缩短 50%、产能提升一倍", standard: "保温时间 4h±0.5h,产能提升 ≥ 80%", method: "DCS 批次记录统计,对比变更前 30 天均值", owner: "车间 · 李主任", result: "待验收" },
|
||||||
@@ -553,6 +670,8 @@ const uploadConfirm = () => {
|
|||||||
const pluginDrawer = ref<boolean>(false);
|
const pluginDrawer = ref<boolean>(false);
|
||||||
const currentPlugin = ref<string>("");
|
const currentPlugin = ref<string>("");
|
||||||
// 插件选择
|
// 插件选择
|
||||||
|
// 注:风险核查(RISK_CHECK)的「AI 预填检查表」交互在 modules/riskCheck.vue 插件内部,
|
||||||
|
// 本页 toggleTool 仅负责打开插件抽屉,无独立 AI 预填入口,故不在此接 aiRiskCheck。
|
||||||
const toggleTool = (t: any) => {
|
const toggleTool = (t: any) => {
|
||||||
if (!tools.value.includes(t.key))
|
if (!tools.value.includes(t.key))
|
||||||
tools.value = tools.value.includes(t.key) ? tools.value.filter((x) => x !== t.key) : [...tools.value, t.key];
|
tools.value = tools.value.includes(t.key) ? tools.value.filter((x) => x !== t.key) : [...tools.value, t.key];
|
||||||
@@ -822,7 +941,7 @@ const onBack = () => {
|
|||||||
<NRadio v-for="t in ['工艺', '设备', '管理']" :key="t" :value="t">{{t}}</NRadio>
|
<NRadio v-for="t in ['工艺', '设备', '管理']" :key="t" :value="t">{{t}}</NRadio>
|
||||||
</div>
|
</div>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="sheet = 'type'">
|
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openTypeSheet">
|
||||||
<Icon icon="lucide:sparkles" class="size-12px" />
|
<Icon icon="lucide:sparkles" class="size-12px" />
|
||||||
AI 判定说明
|
AI 判定说明
|
||||||
</NButton>
|
</NButton>
|
||||||
@@ -836,7 +955,7 @@ const onBack = () => {
|
|||||||
<NRadio v-for="t in ['一般', '重要']" :key="t" :value="t">{{t}}</NRadio>
|
<NRadio v-for="t in ['一般', '重要']" :key="t" :value="t">{{t}}</NRadio>
|
||||||
</div>
|
</div>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="sheet = 'level'">
|
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openLevelSheet">
|
||||||
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }}分) →
|
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }}分) →
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -1478,10 +1597,25 @@ const onBack = () => {
|
|||||||
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
|
||||||
✨ AI 生成 · 需人工确认
|
✨ AI 生成 · 需人工确认
|
||||||
</span>
|
</span>
|
||||||
<NTag type="info" size="small">判定:工艺</NTag>
|
<NTag v-if="typeAiLoading" type="info" size="small">
|
||||||
<NTag size="small">置信度:高</NTag>
|
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 判定中…
|
||||||
|
</NTag>
|
||||||
|
<template v-else>
|
||||||
|
<NTag type="info" size="small">判定:{{ typeAi?.type || '工艺' }}</NTag>
|
||||||
|
<NTag size="small">置信度:{{ typeAi ? `${Math.round(typeAi.confidence * 100)}%` : '高' }}</NTag>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 text-sm leading-relaxed text-slate-700">
|
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||||||
|
<p><b>判定结论:</b>{{ typeAi.conclusion }}</p>
|
||||||
|
<p><b>判定依据:</b></p>
|
||||||
|
<ul class="list-disc space-y-1 pl-5">
|
||||||
|
<li v-for="r in typeAi.hit_rules" :key="r">命中规则:{{ r }}</li>
|
||||||
|
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则:{{ r }}</li>
|
||||||
|
</ul>
|
||||||
|
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用:</b>{{ typeAi.knowledge_refs.join('、') }}。</p>
|
||||||
|
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-3 text-sm leading-relaxed text-slate-700">
|
||||||
<p><b>判定结论:</b>本次变更涉及反应温度(80℃→90℃)、保温时间(8h→4h)等工艺参数调整及 DCS 报警/联锁值修改,未涉及设备本体更换或材质变更,依据变更类型规则判定为「工艺」类变更。</p>
|
<p><b>判定结论:</b>本次变更涉及反应温度(80℃→90℃)、保温时间(8h→4h)等工艺参数调整及 DCS 报警/联锁值修改,未涉及设备本体更换或材质变更,依据变更类型规则判定为「工艺」类变更。</p>
|
||||||
<p><b>判定依据:</b></p>
|
<p><b>判定依据:</b></p>
|
||||||
<ul class="list-disc space-y-1 pl-5">
|
<ul class="list-disc space-y-1 pl-5">
|
||||||
@@ -1540,8 +1674,18 @@ const onBack = () => {
|
|||||||
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-violet-700">
|
<div class="mb-2 flex items-center gap-2 text-sm font-semibold text-violet-700">
|
||||||
<Icon icon="lucide:sparkles" class="size-14px" />
|
<Icon icon="lucide:sparkles" class="size-14px" />
|
||||||
AI 评分分析过程
|
AI 评分分析过程
|
||||||
|
<span v-if="levelAiLoading" class="flex items-center gap-1 text-xs font-normal text-violet-500">
|
||||||
|
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中…
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 text-xs leading-relaxed text-slate-600">
|
<div v-if="levelAiText || levelAiBasis.some((x) => x)" class="space-y-3 text-xs leading-relaxed text-slate-600">
|
||||||
|
<p v-if="levelAiText">{{ levelAiText }}</p>
|
||||||
|
<p v-for="(dim, di) in LEVEL_DIMS" :key="dim.name">
|
||||||
|
<template v-if="levelAiBasis[di]"><b class="text-[#17407F]">{{ dim.name }}:</b>{{ levelAiBasis[di] }}</template>
|
||||||
|
</p>
|
||||||
|
<p class="text-slate-400">(各维度得分均可人工改判,修改留痕;改判后等级自动回填至申请表)</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-3 text-xs leading-relaxed text-slate-600">
|
||||||
<p>我们分析变更内容:缩合反应温度从80℃提高到90℃,反应时间从8小时缩短到4小时。设备材质SS304,设计温度100℃,设计压力0.2MPa,压力不变(常压)。物料为原料A与原料B,未具体说明毒性、腐蚀性。逐维度评分如下。</p>
|
<p>我们分析变更内容:缩合反应温度从80℃提高到90℃,反应时间从8小时缩短到4小时。设备材质SS304,设计温度100℃,设计压力0.2MPa,压力不变(常压)。物料为原料A与原料B,未具体说明毒性、腐蚀性。逐维度评分如下。</p>
|
||||||
<p><b class="text-[#17407F]">1. 设备影响:</b>温度从80℃升至90℃,仍在设计温度100℃范围内,但接近上限。SS304 在 80→90℃ 下腐蚀速率可能略有增加,但变化不大;保守起见给 2 分(中等损害)。</p>
|
<p><b class="text-[#17407F]">1. 设备影响:</b>温度从80℃升至90℃,仍在设计温度100℃范围内,但接近上限。SS304 在 80→90℃ 下腐蚀速率可能略有增加,但变化不大;保守起见给 2 分(中等损害)。</p>
|
||||||
<p><b class="text-[#17407F]">2. 财务影响:</b>变更目的是提升产能,但若设计错误可能导致反应失败、批次报废。温度提高10℃、时间缩短一半,控制不好可能影响产品质量,损失可能中等(1万~10万),取 2 分。</p>
|
<p><b class="text-[#17407F]">2. 财务影响:</b>变更目的是提升产能,但若设计错误可能导致反应失败、批次报废。温度提高10℃、时间缩短一半,控制不好可能影响产品质量,损失可能中等(1万~10万),取 2 分。</p>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { computed, ref } from "vue";
|
|||||||
import { useMessage } from "naive-ui";
|
import { useMessage } from "naive-ui";
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { AiError, aiHazopDeviation } from '@/service/api/ai';
|
||||||
import Ed from "./Ed.vue";
|
import Ed from "./Ed.vue";
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
@@ -79,17 +80,38 @@ const updRow = (ri: number, patch: Partial<any>) => {
|
|||||||
};
|
};
|
||||||
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
|
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
|
||||||
|
|
||||||
const runAi = () => {
|
const runAi = async () => {
|
||||||
if (cur.value.rows.length) return message.info("当前偏离已有分析记录,可手动新增行补充");
|
if (cur.value.rows.length) return message.info("当前偏离已有分析记录,可手动新增行补充");
|
||||||
analyzing.value = true;
|
analyzing.value = true;
|
||||||
setTimeout(() => {
|
try {
|
||||||
|
const node = nodes.value[sel.value.n];
|
||||||
|
const res = await aiHazopDeviation({
|
||||||
|
node_name: node.name,
|
||||||
|
deviation: cur.value.dev,
|
||||||
|
object_name: node.obj,
|
||||||
|
change_context: {
|
||||||
|
change: "缩合反应温度及时间优化",
|
||||||
|
content: "R-101 缩合反应釜反应温度由 80℃ 提升至 90℃、保温时间由 8h 缩短至 4h,DCS 温度报警 / 联锁值随之上调",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const rows = (res?.rows ?? []).map((r) => ({
|
||||||
|
cause: r.cause ?? "", cons: r.consequence ?? "", l: r.l ?? 2, s: r.s ?? 3,
|
||||||
|
safe: r.safeguards ?? "", sug: r.suggestions ?? "",
|
||||||
|
}));
|
||||||
|
if (!rows.length) throw new AiError(0, "AI 未返回分析结果");
|
||||||
|
cur.value.rows = rows;
|
||||||
|
message.success(`AI 已完成「${cur.value.dev}」偏离分析,生成 ${rows.length} 条记录(支持手动修改)`);
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = e instanceof AiError ? e.message : "AI 服务异常";
|
||||||
|
window.$message?.warning(`AI 分析失败:${msg},可手动填写`);
|
||||||
|
// 静态示例数据作 fallback,不阻塞页面
|
||||||
const gen = HAZOP_AI[cur.value.dev] ?? [
|
const gen = HAZOP_AI[cur.value.dev] ?? [
|
||||||
{ cause: "(AI 生成)偏离场景原因分析", cons: "(AI 生成)可能后果描述", l: 2, s: 3, safe: "现有保护层说明", sug: "建议补充的管控措施" },
|
{ cause: "(AI 生成)偏离场景原因分析", cons: "(AI 生成)可能后果描述", l: 2, s: 3, safe: "现有保护层说明", sug: "建议补充的管控措施" },
|
||||||
];
|
];
|
||||||
cur.value.rows = gen.map((x) => ({ ...x }));
|
cur.value.rows = gen.map((x: any) => ({ ...x }));
|
||||||
|
} finally {
|
||||||
analyzing.value = false;
|
analyzing.value = false;
|
||||||
message.success(`AI 已完成「${cur.value.dev}」偏离分析,生成 ${gen.length} 条记录(支持手动修改)`);
|
}
|
||||||
}, 1100);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addRow = () => cur.value.rows.push({ cause: "", cons: "", l: 2, s: 3, safe: "", sug: "" });
|
const addRow = () => cur.value.rows.push({ cause: "", cons: "", l: 2, s: 3, safe: "", sug: "" });
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { computed, ref } from "vue";
|
|||||||
import { useMessage } from "naive-ui";
|
import { useMessage } from "naive-ui";
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { AiError, aiJsaAnalyze } from '@/service/api/ai';
|
||||||
import Ed from "./Ed.vue";
|
import Ed from "./Ed.vue";
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
@@ -66,16 +67,37 @@ const updRow = (si: number, ri: number, patch: Partial<any>) => {
|
|||||||
};
|
};
|
||||||
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
|
const clampLS = (v: string) => Math.max(1, Math.min(5, +v || 1));
|
||||||
|
|
||||||
const runAi = () => {
|
const runAi = async () => {
|
||||||
analyzing.value = true;
|
analyzing.value = true;
|
||||||
setTimeout(() => {
|
try {
|
||||||
steps.value[steps.value.length - 1].rows.push({
|
const hasRows = steps.value.some((s) => s.rows.length > 0);
|
||||||
hazard: "新版操作规程执行不熟练", cons: "误操作导致温度 / 时间偏离工艺窗口", l: 2, s: 3,
|
const res = await aiJsaAnalyze({
|
||||||
safe: "培训考核合格后上岗;首批执行班长旁站确认", sug: "投用首周班长复核关键操作并记录",
|
job_info: { job_name: "变更实施作业", job_location: "一车间 R-101 区域" },
|
||||||
|
steps: steps.value.map((s) => ({ step: s.step, desc: s.desc })),
|
||||||
|
mode: hasRows ? "supplement" : "full",
|
||||||
|
change_context: { content: info.value },
|
||||||
});
|
});
|
||||||
|
let added = 0;
|
||||||
|
(res?.steps ?? []).forEach((st) => {
|
||||||
|
const rows = (st.rows ?? []).map((r) => ({
|
||||||
|
hazard: r.hazard ?? "", cons: r.consequence ?? "", l: r.l ?? 2, s: r.s ?? 3,
|
||||||
|
safe: r.safeguards ?? "", sug: r.suggestions ?? "",
|
||||||
|
}));
|
||||||
|
if (!rows.length) return;
|
||||||
|
// 按步骤名匹配合并,匹配不到则追加为新步骤
|
||||||
|
const target = steps.value.find((s) => st.step && (s.step.includes(st.step) || st.step.includes(s.step)));
|
||||||
|
if (target) target.rows.push(...rows);
|
||||||
|
else steps.value.push({ step: st.step || `步骤${steps.value.length + 1}`, desc: st.desc ?? "", rows });
|
||||||
|
added += rows.length;
|
||||||
|
});
|
||||||
|
if (!added) throw new AiError(0, "AI 未返回分析结果");
|
||||||
|
message.success(`AI 危害因素分析完成:补充 ${added} 条记录(支持手动修改)`);
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = e instanceof AiError ? e.message : "AI 服务异常";
|
||||||
|
window.$message?.warning(`AI 分析失败:${msg},可手动填写`);
|
||||||
|
} finally {
|
||||||
analyzing.value = false;
|
analyzing.value = false;
|
||||||
message.success("AI 危害因素分析完成:补充 1 条记录(支持手动修改)");
|
}
|
||||||
}, 1100);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ const RISK_CHECKS_INIT = [
|
|||||||
},
|
},
|
||||||
{ id: 7, group: "工艺与操作", ans: "否", q: "是否涉及应急预案或消防设施的调整?" },
|
{ id: 7, group: "工艺与操作", ans: "否", q: "是否涉及应急预案或消防设施的调整?" },
|
||||||
];
|
];
|
||||||
|
// 说明:本页面暂无「AI 预填检查表」交互按钮,检查项为静态示例数据,保持现状;
|
||||||
|
// 如需接入可调用 aiRiskCheck(content, rows)(src/service/api/ai.ts,耗时约 1~3 分钟,需处理 42901 限流重试)。
|
||||||
const pluginDone = ref<Record<string, number>>({});
|
const pluginDone = ref<Record<string, number>>({});
|
||||||
const riskChecks = ref<any[]>(RISK_CHECKS_INIT.map((x) => ({ ...x })));
|
const riskChecks = ref<any[]>(RISK_CHECKS_INIT.map((x) => ({ ...x })));
|
||||||
const riskGroups = computed(() => [...new Set(riskChecks.value.map((x) => x.group))]);
|
const riskGroups = computed(() => [...new Set(riskChecks.value.map((x) => x.group))]);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { computed, ref } from "vue";
|
|||||||
import { useMessage } from "naive-ui";
|
import { useMessage } from "naive-ui";
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import { AiError, aiSclAnalyze } from '@/service/api/ai';
|
||||||
import Ed from "./Ed.vue";
|
import Ed from "./Ed.vue";
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
@@ -46,6 +47,40 @@ const exportAll = () => emit("export", groups.value.flatMap((g) => g.rows.map(to
|
|||||||
const save = () => {
|
const save = () => {
|
||||||
message.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
message.success("已手动保存,分析记录数已同步至工具卡片(内容修改实时自动保存,退出不丢失)");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const analyzing = ref(false);
|
||||||
|
const runAi = async () => {
|
||||||
|
analyzing.value = true;
|
||||||
|
try {
|
||||||
|
const res = await aiSclAnalyze({
|
||||||
|
device: { name: SCL_INFO.dev, material_info: SCL_INFO.mat, run_params: SCL_INFO.param },
|
||||||
|
category: cur.value.g,
|
||||||
|
change_context: {
|
||||||
|
change: "缩合反应温度及时间优化",
|
||||||
|
content: "R-101 缩合反应釜反应温度由 80℃ 提升至 90℃、保温时间由 8h 缩短至 4h,DCS 温度报警 / 联锁值随之上调",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let added = 0;
|
||||||
|
(res?.groups ?? []).forEach((g) => {
|
||||||
|
const rows = (g.rows ?? []).map((r) => ({
|
||||||
|
item: r.item ?? "", std: r.std ?? "", cons: r.consequence ?? "", l: r.l ?? 2, s: r.s ?? 3,
|
||||||
|
ctrl: r.controls ?? "", sug: r.suggestions ?? "",
|
||||||
|
}));
|
||||||
|
if (!rows.length) return;
|
||||||
|
const target = groups.value.find((x) => x.g === g.g);
|
||||||
|
if (target) target.rows.push(...rows);
|
||||||
|
else groups.value.push({ g: g.g || cur.value.g, rows });
|
||||||
|
added += rows.length;
|
||||||
|
});
|
||||||
|
if (!added) throw new AiError(0, "AI 未返回分析结果");
|
||||||
|
message.success(`AI 辅助分析完成:新增 ${added} 项检查记录(支持手动修改)`);
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = e instanceof AiError ? e.message : "AI 服务异常";
|
||||||
|
window.$message?.warning(`AI 分析失败:${msg},可手动填写`);
|
||||||
|
} finally {
|
||||||
|
analyzing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
const SCL_INFO = {
|
const SCL_INFO = {
|
||||||
dev: "R-101 缩合反应釜",
|
dev: "R-101 缩合反应釜",
|
||||||
mat: "缩合反应体系(有机溶剂 / 反应物料)",
|
mat: "缩合反应体系(有机溶剂 / 反应物料)",
|
||||||
@@ -152,8 +187,9 @@ const groups = ref([
|
|||||||
<NButton size="small" ghost type="primary" class="text-xs" @click="message.success('SCL 检查表已导出(Word)')">
|
<NButton size="small" ghost type="primary" class="text-xs" @click="message.success('SCL 检查表已导出(Word)')">
|
||||||
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word
|
<Icon icon="material-symbols:download" class="size-14px" /> 导出 Word
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton size="small" type="primary" class="text-xs" @click="message.success('演示:AI 辅助分析完成,已按变更内容核对检查标准(支持手动修改)')">
|
<NButton size="small" type="primary" class="text-xs" :disabled="analyzing" @click="runAi">
|
||||||
<Icon icon="lucide:sparkles" class="size-12px" />
|
<Icon v-if="analyzing" icon="ri:loader-4-fill" class="size-14px animate-spin" />
|
||||||
|
<Icon v-else icon="lucide:sparkles" class="size-12px" />
|
||||||
辅助分析
|
辅助分析
|
||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, h, onMounted, defineComponent } from 'vue';
|
import { ref, computed, h, onMounted } from 'vue';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { NButton, NPopconfirm, NTag, NDropdown, useDialog } from 'naive-ui';
|
import { NButton, NPopconfirm, NTag, NDropdown, useDialog } from 'naive-ui';
|
||||||
|
import type { DataTableColumns, FormRules } from 'naive-ui';
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
@@ -29,7 +30,7 @@ const currentUser = ref<{ id: number, name: string, dept: string[], mobile: stri
|
|||||||
gender: 1,
|
gender: 1,
|
||||||
});
|
});
|
||||||
// 校验规则
|
// 校验规则
|
||||||
const rules = ref({
|
const rules = ref<FormRules>({
|
||||||
name: { required: true, message: '角色名称不能为空', trigger: ['blur'] },
|
name: { required: true, message: '角色名称不能为空', trigger: ['blur'] },
|
||||||
dept: {
|
dept: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
@@ -54,7 +55,265 @@ const pagination = ref<{ page: number, pageSize: number, pageCount: number }>({
|
|||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
pageCount: 0,
|
pageCount: 0,
|
||||||
})
|
})
|
||||||
const columns = ref([
|
const list = ref<{ id: number, name: string, dept: string[], mobile: string, email: string, account: string, role: string | null, dataPermission: string[], status: number, gender: number }[]>([])
|
||||||
|
// 账号角色选项
|
||||||
|
const roleOptions = ref([
|
||||||
|
{ label: '管理员', value: 'admin' },
|
||||||
|
{ label: '普通用户', value: 'user' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const formNodeRef = ref<any>(null);
|
||||||
|
const treeModal = ref<boolean>(false);
|
||||||
|
const currentNode = ref<{ id: string, label: string, key: string, children: any[], shortLabel: string, parent: string, isLeaf: number, sort: number }>({
|
||||||
|
id: '',
|
||||||
|
label: '',
|
||||||
|
key: '',
|
||||||
|
children: [],
|
||||||
|
shortLabel: '',
|
||||||
|
parent: '',
|
||||||
|
isLeaf: 1,
|
||||||
|
sort: 0,
|
||||||
|
})
|
||||||
|
const formNode = ref<{ id: string, label: string, key: string, children: any[], shortLabel: string, parent: string, isLeaf: number, sort: number }>({
|
||||||
|
id: '',
|
||||||
|
label: '',
|
||||||
|
key: '',
|
||||||
|
children: [],
|
||||||
|
shortLabel: '',
|
||||||
|
parent: '',
|
||||||
|
isLeaf: 1,
|
||||||
|
sort: 0,
|
||||||
|
})
|
||||||
|
const currentNodeType = ref<string>('edit')
|
||||||
|
// 树节点规则
|
||||||
|
const treeRules = ref({
|
||||||
|
label: [{ required: true, message: '请输入层级名称', trigger: ['blur'] }],
|
||||||
|
})
|
||||||
|
// 左侧组织树
|
||||||
|
const orgTree = ref<any[]>([
|
||||||
|
{
|
||||||
|
id: "factory", label: "演示工厂", key: "YS", shortLabel: 'YS', parent: '', isLeaf: 0, sort: 0,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "scb", label: "生产部", key: "SCB", shortLabel: 'SCB', parent: '演示工厂', isLeaf: 0, sort: 0,
|
||||||
|
children: [
|
||||||
|
{ id: "cj1", label: "测试车间一", key: "CJ1", shortLabel: 'CJ1', parent: '生产部', isLeaf: 1, sort: 0 },
|
||||||
|
{ id: "cj2", label: "测试车间二", key: "CJ2", shortLabel: 'CJ2', parent: '生产部', isLeaf: 1, sort: 0 },
|
||||||
|
{ id: "cj3", label: "测试车间三", key: "CJ3", shortLabel: 'CJ3', parent: '生产部', isLeaf: 1, sort: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ahb", label: "安环部", key: "AHB", shortLabel: 'AHB', parent: '演示工厂', isLeaf: 0, sort: 0,
|
||||||
|
children: [
|
||||||
|
{ id: "aqk", label: "安全科", key: "AQK", shortLabel: 'AQK', parent: '安环部', isLeaf: 1, sort: 0 },
|
||||||
|
{ id: "hbk", label: "环保科", key: "HBK", shortLabel: 'HBK', parent: '安环部', isLeaf: 1, sort: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sbb", label: "设备部", key: "SBB", shortLabel: 'SBB', parent: '演示工厂', isLeaf: 0, sort: 0,
|
||||||
|
children: [
|
||||||
|
{ id: "sbk", label: "设备科", key: "SBK", shortLabel: 'SBK', parent: '设备部', isLeaf: 1, sort: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "zlb", label: "质量部", key: "ZLB", shortLabel: 'ZLB', parent: '演示工厂', isLeaf: 0, sort: 0,
|
||||||
|
children: [
|
||||||
|
{ id: "zjk", label: "质检科", key: "ZJK", shortLabel: 'ZJK', parent: '质量部', isLeaf: 1, sort: 0 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const defaultExpandedKeys = ref<string[]>(['YS'])
|
||||||
|
const defaultSelectedKeys = ref<string[]>(['YS'])
|
||||||
|
// 右键菜单相关
|
||||||
|
const menuVisible = ref(false)
|
||||||
|
const menuX = ref(0)
|
||||||
|
const menuY = ref(0)
|
||||||
|
// 菜单选项
|
||||||
|
const menuOptions = [
|
||||||
|
{ label: '编辑', key: 'edit' },
|
||||||
|
{ label: '添加子级', key: 'add' },
|
||||||
|
{ label: '删除', key: 'delete' },
|
||||||
|
]
|
||||||
|
const handleSelectKeys = (keys: string[]) => {
|
||||||
|
defaultSelectedKeys.value = keys
|
||||||
|
}
|
||||||
|
// 处理右键点击树节点
|
||||||
|
const treeProps = ({ option }: { option: any }) => {
|
||||||
|
return {
|
||||||
|
onContextmenu(e: MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
currentNode.value = option
|
||||||
|
menuX.value = e.clientX
|
||||||
|
menuY.value = e.clientY
|
||||||
|
menuVisible.value = true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 关闭菜单
|
||||||
|
const closeMenu = () => {
|
||||||
|
menuVisible.value = false
|
||||||
|
}
|
||||||
|
// 点击菜单项
|
||||||
|
const handleMenuSelect = (key: string) => {
|
||||||
|
currentNodeType.value = key
|
||||||
|
switch (key) {
|
||||||
|
case 'edit':
|
||||||
|
formNode.value = currentNode.value
|
||||||
|
treeModal.value = true
|
||||||
|
break
|
||||||
|
case 'add':
|
||||||
|
formNode.value = {
|
||||||
|
id: '',
|
||||||
|
label: '',
|
||||||
|
key: '',
|
||||||
|
children: [],
|
||||||
|
shortLabel: '',
|
||||||
|
parent: currentNode.value.label,
|
||||||
|
isLeaf: 0,
|
||||||
|
sort: 0,
|
||||||
|
}
|
||||||
|
treeModal.value = true
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
const d = dialog.info({
|
||||||
|
title: '删除',
|
||||||
|
content: `确定删除层级「${currentNode.value.label}」吗? 该操作不可恢复`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
d.loading = true
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
d.loading = false
|
||||||
|
resolve(true)
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
d.loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
const renderSwitcherIcon = () => {
|
||||||
|
return h(Icon, { icon: 'formkit:right',class:'size-14px' }, {})
|
||||||
|
}
|
||||||
|
const renderSuffix = ({ option }: { option: any }) => {
|
||||||
|
return h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
type: 'primary',
|
||||||
|
onClick: (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
currentNode.value = option
|
||||||
|
menuX.value = e.clientX
|
||||||
|
menuY.value = e.clientY
|
||||||
|
menuVisible.value = true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: () =>
|
||||||
|
h(Icon, { icon: 'lucide:more-horizontal', class: 'size-14px' }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
// 处理部门选择变化
|
||||||
|
const handleUpdateDept = (value: any) => {
|
||||||
|
currentUser.value.dept = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取列表
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
list.value = [
|
||||||
|
{id:1,name:'姓名1',dept:['YS'],mobile:'13800000000',email:'user1@example.com',account:'user1',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
{id:2,name:'姓名2',dept:[],mobile:'13800000001',email:'user1@example.com',account:'user2',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
{id:2,name:'姓名2',dept:[],mobile:'13800000001',email:'user1@example.com',account:'user2',role:'普通用户',dataPermission:[],gender:1,status:2},
|
||||||
|
{id:3,name:'姓名3',dept:[],mobile:'13800000002',email:'user1@example.com',account:'user3',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
{id:4,name:'姓名4',dept:[],mobile:'13800000003',email:'user1@example.com',account:'user3',role:'普通用户',dataPermission:[],gender:1,status:2},
|
||||||
|
{id:4,name:'姓名4',dept:['YS'],mobile:'13800000003',email:'user1@example.com',account:'user4',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
{id:5,name:'姓名5',dept:[],mobile:'13800000004',email:'user1@example.com',account:'user5',role:'普通用户',dataPermission:[],gender:1,status:2},
|
||||||
|
{id:6,name:'姓名6',dept:['YS'],mobile:'13800000005',email:'user1@example.com',account:'user6',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
{id:7,name:'姓名7',dept:[],mobile:'13800000006',email:'user1@example.com',account:'user7',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
{id:8,name:'姓名8',dept:[],mobile:'13800000007',email:'user1@example.com',account:'user8',role:'普通用户',dataPermission:[],gender:1,status:2},
|
||||||
|
{id:9,name:'姓名9',dept:['YS'],mobile:'13800000008',email:'user1@example.com',account:'user9',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
{id:10,name:'姓名10',dept:[],mobile:'13800000009',email:'user1@example.com',account:'user10',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
{id:11,name:'姓名11',dept:['YS'],mobile:'13800000010',email:'user1@example.com',account:'user11',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
{id:12,name:'姓名12',dept:[],mobile:'13800000011',email:'user1@example.com',account:'user12',role:'普通用户',dataPermission:[],gender:1,status:1},
|
||||||
|
]
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
// 处理数据权限选择变化
|
||||||
|
const handleUpdateChecked = (keys: string[]) => {
|
||||||
|
currentUser.value.dataPermission = keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开添加用户弹框
|
||||||
|
const addUser = () => {
|
||||||
|
currentType.value = 'add';
|
||||||
|
currentUser.value = {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
dept: [],
|
||||||
|
mobile: '',
|
||||||
|
email: '',
|
||||||
|
account: '',
|
||||||
|
role: null,
|
||||||
|
dataPermission: [],
|
||||||
|
status: 1,
|
||||||
|
gender: 1,
|
||||||
|
};
|
||||||
|
userModal.value = true;
|
||||||
|
}
|
||||||
|
// 重置密码
|
||||||
|
const resetPassword = (row: any) => {
|
||||||
|
currentUser.value = {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
dept: row.dept,
|
||||||
|
mobile: row.mobile,
|
||||||
|
email: row.email,
|
||||||
|
account: row.account,
|
||||||
|
role: row.role,
|
||||||
|
dataPermission: row.dataPermission,
|
||||||
|
status: row.status,
|
||||||
|
gender: row.gender,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 编辑用户
|
||||||
|
const handleEdit = (row: any) => {
|
||||||
|
currentType.value = 'edit';
|
||||||
|
currentUser.value = {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
dept: row.dept,
|
||||||
|
mobile: row.mobile,
|
||||||
|
email: row.email,
|
||||||
|
account: row.account,
|
||||||
|
role: row.role,
|
||||||
|
dataPermission: row.dataPermission,
|
||||||
|
status: row.status,
|
||||||
|
gender: row.gender,
|
||||||
|
};
|
||||||
|
userModal.value = true;
|
||||||
|
}
|
||||||
|
// 删除用户
|
||||||
|
const handleDelete = (row: any) => {
|
||||||
|
loading.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
list.value = list.value.filter((item: any) => item.id !== row.id);
|
||||||
|
window.$message?.success('删除成功')
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
const columns = ref<DataTableColumns>([
|
||||||
{
|
{
|
||||||
title: '序号',
|
title: '序号',
|
||||||
key: 'index',
|
key: 'index',
|
||||||
@@ -176,264 +435,6 @@ const columns = ref([
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
const list = ref<{ id: number, name: string, dept: string[], mobile: string, email: string, account: string, role: string | null, dataPermission: string[], status: number, gender: number }[]>([])
|
|
||||||
// 账号角色选项
|
|
||||||
const roleOptions = ref([
|
|
||||||
{ label: '管理员', value: 'admin' },
|
|
||||||
{ label: '普通用户', value: 'user' },
|
|
||||||
])
|
|
||||||
|
|
||||||
const formNodeRef = ref<any>(null);
|
|
||||||
const treeModal = ref<boolean>(false);
|
|
||||||
const currentNode = ref<{ id: string, label: string, key: string, children: any[], shortLabel: string, parent: string, isLeaf: number, sort: number }>({
|
|
||||||
id: '',
|
|
||||||
label: '',
|
|
||||||
key: '',
|
|
||||||
children: [],
|
|
||||||
shortLabel: '',
|
|
||||||
parent: '',
|
|
||||||
isLeaf: 1,
|
|
||||||
sort: 0,
|
|
||||||
})
|
|
||||||
const formNode = ref<{ id: string, label: string, key: string, children: any[], shortLabel: string, parent: string, isLeaf: number, sort: number }>({
|
|
||||||
id: '',
|
|
||||||
label: '',
|
|
||||||
key: '',
|
|
||||||
children: [],
|
|
||||||
shortLabel: '',
|
|
||||||
parent: '',
|
|
||||||
isLeaf: 1,
|
|
||||||
sort: 0,
|
|
||||||
})
|
|
||||||
const currentNodeType = ref<string>('edit')
|
|
||||||
// 树节点规则
|
|
||||||
const treeRules = ref({
|
|
||||||
label: [{ required: true, message: '请输入层级名称', trigger: ['blur'] }],
|
|
||||||
})
|
|
||||||
// 左侧组织树
|
|
||||||
const orgTree = ref([
|
|
||||||
{
|
|
||||||
id: "factory", label: "演示工厂", key: "YS", shortLabel: 'YS', parent: '', isLeaf: 0, sort: 0,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: "scb", label: "生产部", key: "SCB", shortLabel: 'SCB', parent: '演示工厂', isLeaf: 0, sort: 0,
|
|
||||||
children: [
|
|
||||||
{ id: "cj1", label: "测试车间一", key: "CJ1", shortLabel: 'CJ1', parent: '生产部', isLeaf: 1, sort: 0 },
|
|
||||||
{ id: "cj2", label: "测试车间二", key: "CJ2", shortLabel: 'CJ2', parent: '生产部', isLeaf: 1, sort: 0 },
|
|
||||||
{ id: "cj3", label: "测试车间三", key: "CJ3", shortLabel: 'CJ3', parent: '生产部', isLeaf: 1, sort: 0 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ahb", label: "安环部", key: "AHB", shortLabel: 'AHB', parent: '演示工厂', isLeaf: 0, sort: 0,
|
|
||||||
children: [
|
|
||||||
{ id: "aqk", label: "安全科", key: "AQK", shortLabel: 'AQK', parent: '安环部', isLeaf: 1, sort: 0 },
|
|
||||||
{ id: "hbk", label: "环保科", key: "HBK", shortLabel: 'HBK', parent: '安环部', isLeaf: 1, sort: 0 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "sbb", label: "设备部", key: "SBB", shortLabel: 'SBB', parent: '演示工厂', isLeaf: 0, sort: 0,
|
|
||||||
children: [
|
|
||||||
{ id: "sbk", label: "设备科", key: "SBK", shortLabel: 'SBK', parent: '设备部', isLeaf: 1, sort: 0 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "zlb", label: "质量部", key: "ZLB", shortLabel: 'ZLB', parent: '演示工厂', isLeaf: 0, sort: 0,
|
|
||||||
children: [
|
|
||||||
{ id: "zjk", label: "质检科", key: "ZJK", shortLabel: 'ZJK', parent: '质量部', isLeaf: 1, sort: 0 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const defaultExpandedKeys = ref<string[]>(['YS'])
|
|
||||||
const defaultSelectedKeys = ref<string[]>(['YS'])
|
|
||||||
// 右键菜单相关
|
|
||||||
const menuVisible = ref(false)
|
|
||||||
const menuX = ref(0)
|
|
||||||
const menuY = ref(0)
|
|
||||||
// 菜单选项
|
|
||||||
const menuOptions = [
|
|
||||||
{ label: '编辑', key: 'edit' },
|
|
||||||
{ label: '添加子级', key: 'add' },
|
|
||||||
{ label: '删除', key: 'delete' },
|
|
||||||
]
|
|
||||||
const handleSelectKeys = (keys: string[]) => {
|
|
||||||
defaultSelectedKeys.value = keys
|
|
||||||
}
|
|
||||||
// 处理右键点击树节点
|
|
||||||
const treeProps = ({ option }: { option: any }) => {
|
|
||||||
return {
|
|
||||||
onContextmenu(e: MouseEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
currentNode.value = option
|
|
||||||
menuX.value = e.clientX
|
|
||||||
menuY.value = e.clientY
|
|
||||||
menuVisible.value = true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 点击菜单项
|
|
||||||
const handleMenuSelect = (key: string) => {
|
|
||||||
currentNodeType.value = key
|
|
||||||
switch (key) {
|
|
||||||
case 'edit':
|
|
||||||
formNode.value = currentNode.value
|
|
||||||
treeModal.value = true
|
|
||||||
break
|
|
||||||
case 'add':
|
|
||||||
formNode.value = {
|
|
||||||
id: '',
|
|
||||||
label: '',
|
|
||||||
key: '',
|
|
||||||
children: [],
|
|
||||||
shortLabel: '',
|
|
||||||
parent: currentNode.value.label,
|
|
||||||
isLeaf: 0,
|
|
||||||
sort: 0,
|
|
||||||
}
|
|
||||||
treeModal.value = true
|
|
||||||
break
|
|
||||||
case 'delete':
|
|
||||||
const d = dialog.info({
|
|
||||||
title: '删除',
|
|
||||||
content: `确定删除层级「${currentNode.value.label}」吗? 该操作不可恢复`,
|
|
||||||
positiveText: '确定',
|
|
||||||
negativeText: '取消',
|
|
||||||
onPositiveClick: () => {
|
|
||||||
d.loading = true
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
d.loading = false
|
|
||||||
resolve(true)
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onNegativeClick: () => {
|
|
||||||
d.loading = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
closeMenu()
|
|
||||||
}
|
|
||||||
// 关闭菜单
|
|
||||||
const closeMenu = () => {
|
|
||||||
menuVisible.value = false
|
|
||||||
}
|
|
||||||
const renderSwitcherIcon = () => {
|
|
||||||
return h(Icon, { icon: 'formkit:right',class:'size-14px' }, {})
|
|
||||||
}
|
|
||||||
const renderSuffix = ({ option }: { option: any }) => {
|
|
||||||
return h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
text: true,
|
|
||||||
type: 'primary',
|
|
||||||
onClick: (e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
currentNode.value = option
|
|
||||||
menuX.value = e.clientX
|
|
||||||
menuY.value = e.clientY
|
|
||||||
menuVisible.value = true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: () =>
|
|
||||||
h(Icon, { icon: 'lucide:more-horizontal', class: 'size-14px' }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
};
|
|
||||||
// 处理部门选择变化
|
|
||||||
const handleUpdateDept = (value: any) => {
|
|
||||||
currentUser.value.dept = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取列表
|
|
||||||
const getList = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
loading.value = false;
|
|
||||||
list.value = [
|
|
||||||
{id:1,name:'姓名1',dept:['YS'],mobile:'13800000000',email:'user1@example.com',account:'user1',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
{id:2,name:'姓名2',dept:[],mobile:'13800000001',email:'user1@example.com',account:'user2',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
{id:2,name:'姓名2',dept:[],mobile:'13800000001',email:'user1@example.com',account:'user2',role:'普通用户',dataPermission:[],gender:1,status:2},
|
|
||||||
{id:3,name:'姓名3',dept:[],mobile:'13800000002',email:'user1@example.com',account:'user3',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
{id:4,name:'姓名4',dept:[],mobile:'13800000003',email:'user1@example.com',account:'user3',role:'普通用户',dataPermission:[],gender:1,status:2},
|
|
||||||
{id:4,name:'姓名4',dept:['YS'],mobile:'13800000003',email:'user1@example.com',account:'user4',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
{id:5,name:'姓名5',dept:[],mobile:'13800000004',email:'user1@example.com',account:'user5',role:'普通用户',dataPermission:[],gender:1,status:2},
|
|
||||||
{id:6,name:'姓名6',dept:['YS'],mobile:'13800000005',email:'user1@example.com',account:'user6',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
{id:7,name:'姓名7',dept:[],mobile:'13800000006',email:'user1@example.com',account:'user7',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
{id:8,name:'姓名8',dept:[],mobile:'13800000007',email:'user1@example.com',account:'user8',role:'普通用户',dataPermission:[],gender:1,status:2},
|
|
||||||
{id:9,name:'姓名9',dept:['YS'],mobile:'13800000008',email:'user1@example.com',account:'user9',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
{id:10,name:'姓名10',dept:[],mobile:'13800000009',email:'user1@example.com',account:'user10',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
{id:11,name:'姓名11',dept:['YS'],mobile:'13800000010',email:'user1@example.com',account:'user11',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
{id:12,name:'姓名12',dept:[],mobile:'13800000011',email:'user1@example.com',account:'user12',role:'普通用户',dataPermission:[],gender:1,status:1},
|
|
||||||
]
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
// 处理数据权限选择变化
|
|
||||||
const handleUpdateChecked = (keys: string[]) => {
|
|
||||||
currentUser.value.dataPermission = keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开添加用户弹框
|
|
||||||
const addUser = () => {
|
|
||||||
currentType.value = 'add';
|
|
||||||
currentUser.value = {
|
|
||||||
id: 0,
|
|
||||||
name: '',
|
|
||||||
dept: [],
|
|
||||||
mobile: '',
|
|
||||||
email: '',
|
|
||||||
account: '',
|
|
||||||
role: null,
|
|
||||||
dataPermission: [],
|
|
||||||
status: 1,
|
|
||||||
gender: 1,
|
|
||||||
};
|
|
||||||
userModal.value = true;
|
|
||||||
}
|
|
||||||
// 重置密码
|
|
||||||
const resetPassword = (row: any) => {
|
|
||||||
currentUser.value = {
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
dept: row.dept,
|
|
||||||
mobile: row.mobile,
|
|
||||||
email: row.email,
|
|
||||||
account: row.account,
|
|
||||||
role: row.role,
|
|
||||||
dataPermission: row.dataPermission,
|
|
||||||
status: row.status,
|
|
||||||
gender: row.gender,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 编辑用户
|
|
||||||
const handleEdit = (row: any) => {
|
|
||||||
currentType.value = 'edit';
|
|
||||||
currentUser.value = {
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
dept: row.dept,
|
|
||||||
mobile: row.mobile,
|
|
||||||
email: row.email,
|
|
||||||
account: row.account,
|
|
||||||
role: row.role,
|
|
||||||
dataPermission: row.dataPermission,
|
|
||||||
status: row.status,
|
|
||||||
gender: row.gender,
|
|
||||||
};
|
|
||||||
userModal.value = true;
|
|
||||||
}
|
|
||||||
// 删除用户
|
|
||||||
const handleDelete = (row: any) => {
|
|
||||||
loading.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
loading.value = false;
|
|
||||||
list.value = list.value.filter((item: any) => item.id !== row.id);
|
|
||||||
window.$message?.success('删除成功')
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
// 导入用户
|
// 导入用户
|
||||||
const importUser = () => {
|
const importUser = () => {
|
||||||
|
|
||||||
@@ -488,7 +489,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NSpace vertical :size="16">
|
<NSpace vertical :size="16">
|
||||||
<!-- 用户管理 -->
|
<!-- 用户管理 -->
|
||||||
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
<div class="border mb-3 py-2 px-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
<div class="border mb-3 py-2 px-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
|
||||||
@@ -581,8 +582,8 @@ onMounted(() => {
|
|||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="性别">
|
<NFormItem label="性别">
|
||||||
<NRadioGroup v-model:value="currentUser.gender" name="gender">
|
<NRadioGroup v-model:value="currentUser.gender" name="gender">
|
||||||
<n-radio :value="1">男</n-radio>
|
<NRadio :value="1">男</NRadio>
|
||||||
<n-radio :value="2">女</n-radio>
|
<NRadio :value="2">女</NRadio>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="账号" path="account">
|
<NFormItem label="账号" path="account">
|
||||||
@@ -611,7 +612,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="数据权限" path="dataPermission">
|
<NFormItem label="数据权限" path="dataPermission">
|
||||||
<n-tree
|
<NTree
|
||||||
:data="orgTree"
|
:data="orgTree"
|
||||||
block-line
|
block-line
|
||||||
checkable
|
checkable
|
||||||
@@ -622,8 +623,8 @@ onMounted(() => {
|
|||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="状态">
|
<NFormItem label="状态">
|
||||||
<NRadioGroup v-model:value="currentUser.status" name="status">
|
<NRadioGroup v-model:value="currentUser.status" name="status">
|
||||||
<n-radio :value="1">启用</n-radio>
|
<NRadio :value="1">启用</NRadio>
|
||||||
<n-radio :value="2">禁用</n-radio>
|
<NRadio :value="2">禁用</NRadio>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</NForm>
|
</NForm>
|
||||||
@@ -671,14 +672,14 @@ onMounted(() => {
|
|||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="末端层级">
|
<NFormItem label="末端层级">
|
||||||
<NRadioGroup v-model:value="formNode.isLeaf" name="isLeaf">
|
<NRadioGroup v-model:value="formNode.isLeaf" name="isLeaf">
|
||||||
<n-radio :value="1">是</n-radio>
|
<NRadio :value="1">是</NRadio>
|
||||||
<n-radio :value="0">否</n-radio>
|
<NRadio :value="0">否</NRadio>
|
||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="排序">
|
<NFormItem label="排序">
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
class="w-full"
|
|
||||||
v-model:value="formNode.sort"
|
v-model:value="formNode.sort"
|
||||||
|
class="w-full"
|
||||||
:min="0"
|
:min="0"
|
||||||
:show-button="false"
|
:show-button="false"
|
||||||
placeholder="请输入排序"
|
placeholder="请输入排序"
|
||||||
@@ -694,7 +695,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</NModal>
|
</NModal>
|
||||||
</NCard>
|
</NCard>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
+9
-1
@@ -35,7 +35,15 @@ export default defineConfig(configEnv => {
|
|||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 9527,
|
port: 9527,
|
||||||
open: true,
|
open: true,
|
||||||
proxy: createViteProxy(viteEnv, enableProxy)
|
proxy: {
|
||||||
|
...createViteProxy(viteEnv, enableProxy),
|
||||||
|
// ai-server OpenApi(本地 docker ai-server-ai-1,:12333)
|
||||||
|
'/proxy-ai': {
|
||||||
|
target: process.env.AI_SERVER_URL || 'http://127.0.0.1:12333',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: p => p.replace(/^\/proxy-ai/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
port: 9725
|
port: 9725
|
||||||
|
|||||||
Reference in New Issue
Block a user