| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- import { readFileSync, readdirSync, statSync } from 'node:fs'
- import { join, relative } from 'node:path'
- const root = new URL('..', import.meta.url).pathname
- const src = join(root, 'src')
- const requiredFiles = [
- 'src/styles/theme.css',
- 'src/styles/components.css',
- 'src/styles/element-plus.css'
- ]
- const requiredTokens = [
- '--color-brand: #2b1f99',
- '--color-accent: #3ad4d8',
- '--color-bg:',
- '--color-surface:',
- '--color-border:',
- '--color-text:',
- '--radius-xs: 4px',
- '--radius-sm: 6px',
- '--radius-md: 8px'
- ]
- const bannedEmoji = /[📋📌⚡⏳✅❌🎯🚀💡🔥💬]/u
- const bannedDecor = /(radial-gradient|gradient orb|bokeh|blur\(60px\)|border-radius:\s*(1[2-9]|[2-9]\d)px)/
- const bannedTextIcon = /[←→↑↓]/
- const rawHexColor = /#[0-9a-fA-F]{3,8}\b/
- const bannedLegacyPatterns = [
- { re: /page-hero/, msg: '禁止 page-hero(改用 data-toolbar)' },
- { re: /section-badge/, msg: '禁止 section-badge 胶囊徽章' },
- { re: /grade-ring/, msg: '禁止 grade-ring 圆环' },
- { re: /score-hero/, msg: '禁止 score-hero 渐变区' },
- { re: /hint-card/, msg: '禁止 hint-card 大引导卡' },
- { re: /conic-gradient/, msg: '禁止 conic-gradient' }
- ]
- const migratedMarkers = [
- 'clinical-shell',
- 'internal-login',
- 'period-workbench',
- 'period-status-strip',
- 'okr-workbench',
- 'alignment-workbench',
- 'team-okr-workbench',
- 'notification-workbench',
- 'review-workbench',
- 'scoring-worklist',
- 'result-workbench',
- 'history-workbench',
- 'feedback-workbench',
- 'org-workbench',
- 'log-workbench',
- 'dimension-workbench',
- 'template-workbench'
- ]
- function walk(dir) {
- return readdirSync(dir).flatMap((name) => {
- const path = join(dir, name)
- return statSync(path).isDirectory() ? walk(path) : [path]
- })
- }
- const failures = []
- for (const file of requiredFiles) {
- try {
- readFileSync(join(root, file), 'utf8')
- } catch {
- failures.push(`missing required style file: ${file}`)
- }
- }
- const themePath = join(root, 'src/styles/theme.css')
- let theme = ''
- try {
- theme = readFileSync(themePath, 'utf8')
- } catch {}
- for (const token of requiredTokens) {
- if (!theme.includes(token)) failures.push(`missing theme token: ${token}`)
- }
- const main = readFileSync(join(root, 'src/main.js'), 'utf8')
- for (const file of requiredFiles) {
- const importPath = './' + file.replace('src/', '')
- if (!main.includes(importPath)) failures.push(`main.js must import ${importPath}`)
- }
- const layout = readFileSync(join(root, 'src/components/MainLayout.vue'), 'utf8')
- const login = readFileSync(join(root, 'src/views/auth/LoginView.vue'), 'utf8')
- const periodList = readFileSync(join(root, 'src/views/period/PeriodListView.vue'), 'utf8')
- const periodBanner = readFileSync(join(root, 'src/components/PeriodBanner.vue'), 'utf8')
- const myOkr = readFileSync(join(root, 'src/views/okr/MyOkrView.vue'), 'utf8')
- const teamOkr = readFileSync(join(root, 'src/views/okr/TeamOkrView.vue'), 'utf8')
- const scoringEdit = readFileSync(join(root, 'src/views/scoring/ScoringEditView.vue'), 'utf8')
- const scoringList = readFileSync(join(root, 'src/views/scoring/ScoringListView.vue'), 'utf8')
- const myResult = readFileSync(join(root, 'src/views/scoring/MyResultView.vue'), 'utf8')
- if (!layout.includes('clinical-shell')) failures.push('MainLayout must use clinical-shell')
- if (!layout.includes('clinical-sidebar')) failures.push('MainLayout must use clinical-sidebar')
- if (!login.includes('internal-login')) failures.push('LoginView must use internal-login')
- if (login.includes('radial-gradient')) failures.push('LoginView must not use radial-gradient')
- if (!periodList.includes('period-workbench')) failures.push('PeriodListView must use period-workbench')
- if (!periodList.includes('period-metrics')) failures.push('PeriodListView must show period-metrics')
- if (!periodBanner.includes('period-status-strip')) failures.push('PeriodBanner must use period-status-strip')
- if (!myOkr.includes('okr-workbench')) failures.push('MyOkrView must use okr-workbench')
- if (!teamOkr.includes('team-okr-workbench')) failures.push('TeamOkrView must use team-okr-workbench')
- if (!teamOkr.includes('@update:model-value="selectPeriod"')) failures.push('TeamOkrView period switch must reload subordinate OKRs')
- if (!scoringEdit.includes('review-workbench')) failures.push('ScoringEditView must use review-workbench')
- if (!scoringList.includes('scoring-worklist')) failures.push('ScoringListView must use scoring-worklist')
- // v2 信息架构断言
- if (!myOkr.includes('data-toolbar')) failures.push('MyOkrView must use data-toolbar (replaced page-hero)')
- if (!myOkr.includes('data-table')) failures.push('MyOkrView must use data-table (table-based O/KR)')
- if (!myOkr.includes('side-drawer__content') && !myOkr.includes('el-drawer')) {
- failures.push('MyOkrView must use side drawer for KR detail/progress')
- }
- if (!scoringEdit.includes('compare-grid-3')) failures.push('ScoringEditView must use compare-grid-3 (3-column comparison)')
- if (!scoringEdit.includes('section-band')) failures.push('ScoringEditView must use section-band')
- if (!scoringEdit.includes('review-summary-bar')) failures.push('ScoringEditView must use review-summary-bar')
- if (!myResult.includes('metric-bar')) failures.push('MyResultView must use metric-bar (replaced grade-ring)')
- if (!myResult.includes('history-trend')) failures.push('MyResultView must use history-trend')
- if (myResult.includes('conic-gradient')) failures.push('MyResultView must not use conic-gradient (grade-ring removed)')
- if (!login.includes('underline-input')) failures.push('LoginView must use underline-input')
- if (!login.includes('login-brand')) failures.push('LoginView must use login-brand (full-screen split)')
- const v2LegacyCheckFiles = new Map([
- ['src/components/MainLayout.vue', layout],
- ['src/views/auth/LoginView.vue', login],
- ['src/views/okr/MyOkrView.vue', myOkr],
- ['src/views/scoring/ScoringEditView.vue', scoringEdit],
- ['src/views/scoring/MyResultView.vue', myResult]
- ])
- for (const [file, text] of v2LegacyCheckFiles) {
- for (const { re, msg } of bannedLegacyPatterns) {
- if (re.test(text)) failures.push(`${file}: ${msg}`)
- }
- }
- const requiredClasses = new Map([
- ['src/views/scoring/MyResultView.vue', 'result-workbench'],
- ['src/views/scoring/HistoryView.vue', 'history-workbench'],
- ['src/views/feedback/FeedbackView.vue', 'feedback-workbench'],
- ['src/views/org/OrgView.vue', 'org-workbench'],
- ['src/views/system/LogView.vue', 'log-workbench'],
- ['src/views/system/DimensionConfigView.vue', 'dimension-workbench'],
- ['src/views/period/KpiTemplateView.vue', 'template-workbench'],
- ['src/views/okr/AlignmentView.vue', 'alignment-workbench'],
- ['src/views/notification/NotificationView.vue', 'notification-workbench']
- ])
- for (const [file, className] of requiredClasses) {
- const text = readFileSync(join(root, file), 'utf8')
- if (!text.includes(className)) failures.push(`${file} must use ${className}`)
- }
- const vueAndCssFiles = walk(src).filter((file) => /\.(vue|css)$/.test(file))
- for (const file of vueAndCssFiles) {
- const text = readFileSync(file, 'utf8')
- const rel = relative(root, file)
- const mustFollowMedtechRules = rel.startsWith('src/styles/') || migratedMarkers.some((marker) => text.includes(marker))
- if (mustFollowMedtechRules && bannedEmoji.test(text)) failures.push(`banned emoji in ${rel}`)
- if (mustFollowMedtechRules && bannedDecor.test(text)) failures.push(`banned decorative style in ${rel}`)
- if (mustFollowMedtechRules && rel !== 'src/styles/theme.css' && rawHexColor.test(text)) failures.push(`raw hex color in migrated file: ${rel}`)
- if (mustFollowMedtechRules && bannedTextIcon.test(text)) failures.push(`text arrow used as icon in ${rel}`)
- }
- if (failures.length) {
- console.error(failures.join('\n'))
- process.exit(1)
- }
- console.log('medtech theme checks passed')
|