Просмотр исходного кода

style: redesign period lifecycle workspace

wangkangyjy 1 месяц назад
Родитель
Сommit
77e643f847

+ 5 - 0
frontend/scripts/verify-medtech-theme.mjs

@@ -77,11 +77,16 @@ for (const file of requiredFiles) {
 
 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')
 
 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')
 
 const vueAndCssFiles = walk(src).filter((file) => /\.(vue|css)$/.test(file))
 for (const file of vueAndCssFiles) {

+ 14 - 14
frontend/src/components/PeriodBanner.vue

@@ -1,7 +1,7 @@
 <template>
-  <div class="period-banner" :class="'banner-' + active.status">
+  <div class="period-status-strip" :class="'is-' + active.status">
     <div class="pb-left">
-      <span class="pb-icon">{{ statusIcon }}</span>
+      <span class="status-dot" :class="statusDotClass"></span>
       <div class="pb-info">
         <div class="pb-name">{{ active.name }}</div>
         <div class="pb-date">{{ active.startDate }} ~ {{ active.endDate }}</div>
@@ -53,8 +53,8 @@ const active = computed(() => {
 
 const switchablePeriods = computed(() => activePeriods.value)
 
-const statusIcon = computed(() => {
-  return { DRAFT: '', OKR_ALIGN: '', EXECUTING: '', ASSESSING: '' }[active.value.status] || ''
+const statusDotClass = computed(() => {
+  return { DRAFT: '', OKR_ALIGN: 'is-active', EXECUTING: 'is-success', ASSESSING: 'is-warning' }[active.value.status] || ''
 })
 
 const statusLabel = computed(() => {
@@ -79,20 +79,20 @@ function handleSwitch(id) {
 </script>
 
 <style scoped>
-.period-banner {
+.period-status-strip {
   display: flex; align-items: center; justify-content: space-between;
   padding: 14px 20px; border-radius: 10px; margin-bottom: 20px;
   box-shadow: 0 1px 4px rgba(0,0,0,0.04);
+  background: var(--color-surface);
+  border: 1px solid var(--color-border-soft);
 }
-.banner-DRAFT       { background: #f5f5f5; border: 1px solid #e0e0e0; }
-.banner-OKR_ALIGN   { background: #e0f7fa; border: 1px solid #b2ebf2; }
-.banner-EXECUTING   { background: #e8f5e9; border: 1px solid #c8e6c9; }
-.banner-ASSESSING   { background: #fef7e8; border: 1px solid #f5dab1; }
+.period-status-strip.is-OKR_ALIGN { border-left: 3px solid var(--color-accent); }
+.period-status-strip.is-EXECUTING { border-left: 3px solid var(--color-success); }
+.period-status-strip.is-ASSESSING { border-left: 3px solid var(--color-warning); }
 
 .pb-left { display: flex; align-items: center; gap: 12px; }
-.pb-icon { font-size: 24px; }
-.pb-name { font-size: 16px; font-weight: 700; color: #1a1a2e; }
-.pb-date { font-size: 12px; color: #888; margin-top: 2px; }
+.pb-name { font-size: 16px; font-weight: 700; color: var(--color-text); }
+.pb-date { font-size: 12px; color: var(--color-text-secondary); margin-top: 2px; }
 
 .pb-switch-btn {
   display: inline-flex; align-items: center; gap: 4px;
@@ -100,8 +100,8 @@ function handleSwitch(id) {
   cursor: pointer; font-size: 13px; color: #555; font-weight: 500;
   border: 1px solid #ddd; transition: all 0.15s;
 }
-.pb-switch-btn:hover { border-color: #2b1f99; color: #2b1f99; }
+.pb-switch-btn:hover { border-color: var(--color-brand); color: var(--color-brand); }
 
 .pbd-name { margin-right: 8px; font-size: 13px; }
-.is-active .pbd-name { color: #2b1f99; font-weight: 600; }
+.is-active .pbd-name { color: var(--color-brand); font-weight: 600; }
 </style>

+ 29 - 10
frontend/src/views/period/PeriodListView.vue

@@ -1,15 +1,34 @@
 <template>
-  <div class="page">
-    <div class="page-hero">
+  <div class="app-page period-workbench">
+    <div class="app-page-header page-hero">
       <div class="hero-info">
-        <h2>考核周期</h2>
-        <p>管理考核节奏,推动 OKR 对齐</p>
+        <h2 class="app-page-title">考核周期</h2>
+        <p class="app-page-subtitle">管理考核节奏,推动 OKR 对齐</p>
       </div>
       <el-button v-if="auth.isAdmin" type="primary" size="large" @click="showCreate = true" round>
         创建新周期
       </el-button>
     </div>
 
+    <div class="period-metrics metric-row" v-if="periods.length">
+      <div class="metric-card">
+        <span class="metric-card__label">进行中</span>
+        <strong>{{ active.length }}</strong>
+      </div>
+      <div class="metric-card">
+        <span class="metric-card__label">OKR 对齐</span>
+        <strong>{{ okrAlign.length }}</strong>
+      </div>
+      <div class="metric-card">
+        <span class="metric-card__label">考评中</span>
+        <strong>{{ assessing.length }}</strong>
+      </div>
+      <div class="metric-card">
+        <span class="metric-card__label">已归档</span>
+        <strong>{{ archived.length }}</strong>
+      </div>
+    </div>
+
     <div v-if="periods.length === 0" class="empty-state">
       <el-empty description="暂无考核周期">
         <template #extra>
@@ -234,6 +253,7 @@ const upcoming = computed(() => periods.value.filter(p => p.status !== 'ARCHIVED
 
 // Sub-groups within 进行中
 const executing = computed(() => active.value.filter(p => p.status === 'EXECUTING' || p.status === 'ASSESSING'))
+const assessing = computed(() => active.value.filter(p => p.status === 'ASSESSING'))
 const okrAlign = computed(() => active.value.filter(p => p.status === 'OKR_ALIGN'))
 const drafts = computed(() => active.value.filter(p => p.status === 'DRAFT'))
 
@@ -321,11 +341,10 @@ function statusGuide(p) {
 </script>
 
 <style scoped>
-.page { width: 100%; max-width: 960px; }
-.page-hero { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }
-.hero-info h2 { font-size: 28px; font-weight: 700; color: #1a1a2e; margin: 0; }
-.hero-info p { font-size: 14px; color: #888; margin: 4px 0 0; }
-.empty-state { display: flex; align-items: center; justify-content: center; min-height: 300px; background: #fff; border-radius: 12px; }
+.period-workbench { max-width: 960px; }
+.page-hero { margin-bottom: 18px; }
+.period-metrics { margin-bottom: 18px; }
+.empty-state { min-height: 300px; }
 .empty-hint { font-size: 13px; color: #bbb; }
 
 /* Tabs */
@@ -339,7 +358,7 @@ function statusGuide(p) {
 /* Category */
 .category { margin-bottom: 32px; }
 .cat-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
-.cat-badge { padding: 5px 16px; border-radius: 16px; font-size: 14px; font-weight: 700; color: #fff; }
+.cat-badge { padding: 5px 16px; border-radius: 8px; font-size: 14px; font-weight: 700; color: #fff; }
 .cat-badge.active { background: #2b1f99; }
 .cat-badge.upcoming { background: #909399; }
 .cat-badge.archived { background: #b0b4ba; }