Browse Source

chore: auto bump version and changelog [skip ci]

iwt 19 hours ago
parent
commit
35c2800bca

+ 301 - 152
cfc-frontend/components/address-picker.vue

@@ -1,38 +1,66 @@
 <template>
   <view class="address-picker">
-    <view class="picker-row" @click="showProvincePicker">
-      <text class="picker-label">省份:</text>
-      <text class="picker-value">{{ selectedProvince ? selectedProvince.province : '请选择' }}</text>
-      <text class="picker-arrow">▶</text>
+    <!-- 显示当前选中的地址 -->
+    <view class="current-address" @click="openPicker">
+      <text class="address-text">{{ currentAddress || '请选择地址' }}</text>
+      <text class="address-arrow">›</text>
     </view>
-    
-    <view class="picker-row" @click="showCityPicker" v-if="selectedProvince">
-      <text class="picker-label">城市:</text>
-      <text class="picker-value">{{ selectedCity ? selectedCity.city : '请选择' }}</text>
-      <text class="picker-arrow">▶</text>
-    </view>
-    
-    <view class="picker-row" @click="showDistrictPicker" v-if="selectedCity">
-      <text class="picker-label">区县:</text>
-      <text class="picker-value">{{ selectedDistrict ? selectedDistrict.district : '请选择' }}</text>
-      <text class="picker-arrow">▶</text>
-    </view>
-    
-    <view class="picker-row" @click="showStreetPicker" v-if="selectedDistrict">
-      <text class="picker-label">街道:</text>
-      <text class="picker-value">{{ selectedStreet ? selectedStreet.street : '请选择' }}</text>
-      <text class="picker-arrow">▶</text>
-    </view>
-    
-    <view class="address-display" v-if="fullAddress">
-      <text class="address-label">完整地址:</text>
-      <text class="address-text">{{ fullAddress }}</text>
+
+    <!-- 地址选择面板 -->
+    <view class="picker-mask" v-if="showPicker" @click="closePicker">
+      <view class="picker-panel" @click.stop>
+        <!-- 面板头部 -->
+        <view class="picker-header">
+          <text class="picker-btn" @click="closePicker">取消</text>
+          <text class="picker-title">选择地址</text>
+          <text class="picker-btn picker-confirm" @click="confirmAddress">确定</text>
+        </view>
+
+        <!-- 面包屑导航 -->
+        <view class="picker-breadcrumb">
+          <text
+            :class="['crumb', currentLevel === 0 ? 'crumb-active' : '']"
+            @click="jumpToLevel(0)"
+          >{{ tempProvince || '省份' }}</text>
+          <text v-if="tempProvince" class="crumb-arrow">›</text>
+          <text
+            :class="['crumb', currentLevel === 1 ? 'crumb-active' : '']"
+            @click="jumpToLevel(1)"
+          >{{ tempCity || '城市' }}</text>
+          <text v-if="tempCity" class="crumb-arrow">›</text>
+          <text
+            :class="['crumb', currentLevel === 2 ? 'crumb-active' : '']"
+            @click="jumpToLevel(2)"
+          >{{ tempDistrict || '区县' }}</text>
+          <text v-if="tempDistrict" class="crumb-arrow">›</text>
+          <text
+            :class="['crumb', currentLevel === 3 ? 'crumb-active' : '']"
+            @click="jumpToLevel(3)"
+          >{{ tempStreet || '街道' }}</text>
+        </view>
+
+        <!-- 选择列表 -->
+        <scroll-view class="picker-list" scroll-y>
+          <view
+            v-for="(item, idx) in currentItems"
+            :key="idx"
+            :class="['picker-item', isSelected(item) ? 'picker-item-selected' : '']"
+            @click="selectItem(item)"
+          >
+            <text class="item-text">{{ getItemName(item) }}</text>
+            <text v-if="isSelected(item)" class="item-check">✓</text>
+          </view>
+          <view v-if="loading" class="picker-loading">
+            <text>加载中...</text>
+          </view>
+        </scroll-view>
+      </view>
     </view>
   </view>
 </template>
 
 <script>
-import { getProvinces, getStreetChildren, getStreetPath } from '@/utils/api.js'
+import { getProvinces, getStreetChildren } from '@/utils/api.js'
 
 export default {
   name: 'AddressPicker',
@@ -44,22 +72,30 @@ export default {
   },
   data() {
     return {
+      showPicker: false,
+      currentLevel: 0, // 0=省份, 1=城市, 2=区县, 3=街道
+      loading: false,
+      // 数据列表
       provinces: [],
       cities: [],
       districts: [],
       streets: [],
-      selectedProvince: null,
-      selectedCity: null,
-      selectedDistrict: null,
-      selectedStreet: null
+      // 临时选中项
+      tempProvince: null,
+      tempCity: null,
+      tempDistrict: null,
+      tempStreet: null,
+      // 完整地址文本
+      currentAddress: ''
     }
   },
   computed: {
-    fullAddress() {
-      if (this.selectedStreet) {
-        return this.selectedStreet.fullName
-      }
-      return ''
+    currentItems() {
+      if (this.currentLevel === 0) return this.provinces
+      if (this.currentLevel === 1) return this.cities
+      if (this.currentLevel === 2) return this.districts
+      if (this.currentLevel === 3) return this.streets
+      return []
     }
   },
   watch: {
@@ -84,7 +120,7 @@ export default {
         console.error('加载省份失败', e)
       }
     },
-    
+
     async loadCities(provinceId) {
       try {
         const res = await getStreetChildren(provinceId)
@@ -93,7 +129,7 @@ export default {
         console.error('加载城市失败', e)
       }
     },
-    
+
     async loadDistricts(cityId) {
       try {
         const res = await getStreetChildren(cityId)
@@ -102,7 +138,7 @@ export default {
         console.error('加载区县失败', e)
       }
     },
-    
+
     async loadStreets(districtId) {
       try {
         const res = await getStreetChildren(districtId)
@@ -111,122 +147,142 @@ export default {
         console.error('加载街道失败', e)
       }
     },
-    
+
     async loadAddressById(streetId) {
       try {
-        const res = await getStreetPath(streetId)
+        const res = await getStreetChildren(streetId)
         const path = res.data || []
-        
-        if (path.length >= 1) this.selectedProvince = path[0]
-        if (path.length >= 2) this.selectedCity = path[1]
-        if (path.length >= 3) this.selectedDistrict = path[2]
-        if (path.length >= 4) this.selectedStreet = path[3]
-        
+
+        if (path.length >= 4) {
+          this.tempProvince = path[0]
+          this.tempCity = path[1]
+          this.tempDistrict = path[2]
+          this.tempStreet = path[3]
+          this.currentAddress = path[3].fullName || this.buildAddress(path)
+        }
+
         // 加载下级选项
-        if (this.selectedProvince) await this.loadCities(this.selectedProvince.id)
-        if (this.selectedCity) await this.loadDistricts(this.selectedCity.id)
-        if (this.selectedDistrict) await this.loadStreets(this.selectedDistrict.id)
+        if (this.tempProvince) await this.loadCities(this.tempProvince.id)
+        if (this.tempCity) await this.loadDistricts(this.tempCity.id)
+        if (this.tempDistrict) await this.loadStreets(this.tempDistrict.id)
       } catch (e) {
         console.error('加载地址路径失败', e)
       }
     },
-    
-    showProvincePicker() {
-      uni.showActionSheet({
-        itemList: this.provinces.map(p => p.province),
-        success: async (res) => {
-          this.selectedProvince = this.provinces[res.tapIndex]
-          this.selectedCity = null
-          this.selectedDistrict = null
-          this.selectedStreet = null
-          this.cities = []
-          this.districts = []
-          this.streets = []
-          
-          await this.loadCities(this.selectedProvince.id)
-          this.emitChange()
-        }
-      })
+
+    buildAddress(path) {
+      if (!path || path.length === 0) return ''
+      var parts = []
+      if (path[0]) parts.push(path[0].province || path[0].name)
+      if (path[1]) parts.push(path[1].city || path[1].name)
+      if (path[2]) parts.push(path[2].district || path[2].name)
+      if (path[3]) parts.push(path[3].street || path[3].name)
+      return parts.join('')
     },
-    
-    showCityPicker() {
-      if (this.cities.length === 0) {
-        uni.showToast({ title: '请先选择省份', icon: 'none' })
-        return
+
+    openPicker() {
+      this.showPicker = true
+      if (!this.tempProvince && this.provinces.length === 0) {
+        this.loadProvinces()
       }
-      
-      uni.showActionSheet({
-        itemList: this.cities.map(c => c.city),
-        success: async (res) => {
-          this.selectedCity = this.cities[res.tapIndex]
-          this.selectedDistrict = null
-          this.selectedStreet = null
-          this.districts = []
-          this.streets = []
-          
-          await this.loadDistricts(this.selectedCity.id)
-          this.emitChange()
-        }
-      })
     },
-    
-    showDistrictPicker() {
-      if (this.districts.length === 0) {
-        uni.showToast({ title: '请先选择城市', icon: 'none' })
-        return
+
+    closePicker() {
+      this.showPicker = false
+    },
+
+    jumpToLevel(level) {
+      if (level < this.currentLevel) {
+        this.currentLevel = level
       }
-      
-      uni.showActionSheet({
-        itemList: this.districts.map(d => d.district),
-        success: async (res) => {
-          this.selectedDistrict = this.districts[res.tapIndex]
-          this.selectedStreet = null
-          this.streets = []
-          
-          await this.loadStreets(this.selectedDistrict.id)
-          this.emitChange()
-        }
-      })
     },
-    
-    showStreetPicker() {
-      if (this.streets.length === 0) {
-        uni.showToast({ title: '请先选择区县', icon: 'none' })
-        return
+
+    selectItem(item) {
+      if (this.currentLevel === 0) {
+        this.tempProvince = item
+        this.tempCity = null
+        this.tempDistrict = null
+        this.tempStreet = null
+        this.cities = []
+        this.districts = []
+        this.streets = []
+        this.currentLevel = 1
+        this.loadCities(item.id)
+      } else if (this.currentLevel === 1) {
+        this.tempCity = item
+        this.tempDistrict = null
+        this.tempStreet = null
+        this.districts = []
+        this.streets = []
+        this.currentLevel = 2
+        this.loadDistricts(item.id)
+      } else if (this.currentLevel === 2) {
+        this.tempDistrict = item
+        this.tempStreet = null
+        this.streets = []
+        this.currentLevel = 3
+        this.loadStreets(item.id)
+      } else if (this.currentLevel === 3) {
+        this.tempStreet = item
+        this.confirmAddress()
       }
-      
-      uni.showActionSheet({
-        itemList: this.streets.map(s => s.street),
-        success: (res) => {
-          this.selectedStreet = this.streets[res.tapIndex]
-          this.emitChange()
-        }
-      })
     },
-    
+
+    confirmAddress() {
+      this.updateAddressText()
+      this.emitChange()
+      this.closePicker()
+    },
+
+    updateAddressText() {
+      var parts = []
+      if (this.tempProvince) parts.push(this.tempProvince.province || this.tempProvince.name)
+      if (this.tempCity) parts.push(this.tempCity.city || this.tempCity.name)
+      if (this.tempDistrict) parts.push(this.tempDistrict.district || this.tempDistrict.name)
+      if (this.tempStreet) parts.push(this.tempStreet.street || this.tempStreet.name)
+      this.currentAddress = parts.join('')
+    },
+
     emitChange() {
-      const streetId = this.selectedStreet ? this.selectedStreet.id : null
+      const streetId = this.tempStreet ? this.tempStreet.id : null
       const addressData = {
         streetId: streetId,
-        province: this.selectedProvince ? this.selectedProvince.province : null,
-        city: this.selectedCity ? this.selectedCity.city : null,
-        district: this.selectedDistrict ? this.selectedDistrict.district : null,
-        street: this.selectedStreet ? this.selectedStreet.street : null,
-        fullName: this.fullAddress
+        province: this.tempProvince ? this.tempProvince.province : null,
+        city: this.tempCity ? this.tempCity.city : null,
+        district: this.tempDistrict ? this.tempDistrict.district : null,
+        street: this.tempStreet ? this.tempStreet.street : null,
+        fullName: this.currentAddress
       }
-      
+
       this.$emit('input', streetId)
       this.$emit('change', addressData)
     },
-    
+
+    getItemName(item) {
+      if (!item) return ''
+      return item.province || item.city || item.district || item.street || item.name || ''
+    },
+
+    isSelected(item) {
+      var name = this.getItemName(item)
+      if (this.currentLevel === 0) return this.tempProvince && this.getItemName(this.tempProvince) === name
+      if (this.currentLevel === 1) return this.tempCity && this.getItemName(this.tempCity) === name
+      if (this.currentLevel === 2) return this.tempDistrict && this.getItemName(this.tempDistrict) === name
+      if (this.currentLevel === 3) return this.tempStreet && this.getItemName(this.tempStreet) === name
+      return false
+    },
+
     reset() {
-      this.selectedProvince = null
-      this.selectedCity = null
-      this.selectedDistrict = null
-      this.selectedStreet = null
+      this.tempProvince = null
+      this.tempCity = null
+      this.tempDistrict = null
+      this.tempStreet = null
+      this.currentAddress = ''
+      this.provinces = []
       this.cities = []
       this.districts = []
       this.streets = []
+      this.currentLevel = 0
       this.emitChange()
     }
   }
@@ -235,51 +291,144 @@ export default {
 
 <style scoped>
 .address-picker {
+  width: 100%;
+}
+
+.current-address {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 24rpx 20rpx;
+  background: #f8f8f8;
+  border-radius: 12rpx;
+  border: 2rpx solid #eee;
+}
+
+.address-text {
+  font-size: 28rpx;
+  color: #333;
+  flex: 1;
+}
+
+.address-arrow {
+  font-size: 32rpx;
+  color: #999;
+  margin-left: 10rpx;
+}
+
+/* 面板遮罩 */
+.picker-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 999;
+  display: flex;
+  align-items: flex-end;
+}
+
+.picker-panel {
+  width: 100%;
   background: #fff;
-  padding: 20rpx;
+  border-radius: 24rpx 24rpx 0 0;
+  max-height: 70vh;
+  display: flex;
+  flex-direction: column;
 }
 
-.picker-row {
+/* 面板头部 */
+.picker-header {
   display: flex;
   align-items: center;
-  padding: 20rpx 0;
+  justify-content: space-between;
+  padding: 30rpx 30rpx 20rpx;
   border-bottom: 1px solid #eee;
 }
 
-.picker-label {
+.picker-btn {
   font-size: 28rpx;
   color: #666;
-  width: 140rpx;
+  padding: 10rpx 20rpx;
 }
 
-.picker-value {
-  flex: 1;
-  font-size: 28rpx;
+.picker-confirm {
+  color: #f97316;
+  font-weight: bold;
+}
+
+.picker-title {
+  font-size: 32rpx;
+  font-weight: bold;
   color: #333;
 }
 
-.picker-arrow {
-  font-size: 24rpx;
-  color: #999;
-  margin-left: 10rpx;
+/* 面包屑导航 */
+.picker-breadcrumb {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 30rpx;
+  background: #fafafa;
+  border-bottom: 1px solid #eee;
+  flex-wrap: wrap;
 }
 
-.address-display {
-  margin-top: 20rpx;
-  padding: 20rpx;
-  background: #f5f5f5;
-  border-radius: 8rpx;
+.crumb {
+  font-size: 26rpx;
+  color: #666;
+  padding: 8rpx 16rpx;
+  background: #eee;
+  border-radius: 20rpx;
 }
 
-.address-label {
-  font-size: 24rpx;
-  color: #999;
-  display: block;
-  margin-bottom: 10rpx;
+.crumb-active {
+  background: #f97316;
+  color: #fff;
 }
 
-.address-text {
+.crumb-arrow {
   font-size: 28rpx;
+  color: #999;
+  margin: 0 8rpx;
+}
+
+/* 选择列表 */
+.picker-list {
+  flex: 1;
+  min-height: 300rpx;
+  max-height: 500rpx;
+  padding: 20rpx 0;
+}
+
+.picker-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 28rpx 40rpx;
+  border-bottom: 1px solid #f5f5f5;
+}
+
+.item-text {
+  font-size: 30rpx;
   color: #333;
 }
+
+.item-check {
+  font-size: 32rpx;
+  color: #f97316;
+  font-weight: bold;
+}
+
+.picker-item-selected .item-text {
+  color: #f97316;
+  font-weight: bold;
+}
+
+.picker-loading {
+  padding: 40rpx;
+  text-align: center;
+  color: #999;
+  font-size: 28rpx;
+}
 </style>

+ 1 - 1
cfc-frontend/pages/profile/service-role-apply.vue

@@ -261,7 +261,7 @@ export default {
   gap: 20rpx;
 }
 .role-card {
-  width: calc(33.33% - 14rpx);
+  width: calc((100% - 40rpx) / 3);
   background: #f8f8f8;
   border-radius: 12rpx;
   padding: 24rpx 0;

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-fbad0d85b5e023f36a87a177558164d65b1ac3f0
+521f307c244384c2734f630f41f19aa8fbfd24f0

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1568",
+  "version": "1.0.1569",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1568",
+      "version": "1.0.1569",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1569",
+  "version": "1.0.1570",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 27 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,33 @@
 
 ---
 
+## v1.0.1570 (2026-10-11)
+
+### 新功能
+- 浠艾福桌面宠物 v1.0 完成(WPF + WebView2 架构)
+- 桌面宠物设计文档 v1.1 - 支持用户自定义状态
+- 桌面宠物设计文档 v1.0
+
+### 其他
+- - 架构:WPF (.NET 6) 主进程 + WebView2 渲染层,Bridge via cfcPetAPI
+- - 状态映射:14 种核心状态 + 用户自定义状态(custom_state_1~10)
+- - API 对接:POST /api/tasks/today、/api/energy/overview、/api/daily/checkin
+- - 形象配置:JSON Schema (PetConfig),支持浠宝/福宝预设 + 用户自定义 PNG
+- - 交互闭环:喝水提醒 → 点击确认 → API 打卡 → 庆祝动画
+- - 托盘常驻:最小化到托盘,右键菜单(设置/显示/退出),双击唤出
+- - Token 存储:Windows DPAPI 加密,%APPDATA%\XafPet\token.bin
+- - 动画库:20 个 CSS keyframes(idle/happy/busy/sad/celebrate 等)
+- - 测试:xUnit + Moq 单元测试 18 个(PetConfig/ApiClient/StateManager)
+- Windows 构建命令:
+-   dotnet build XafPet.Desktop.sln
+-   dotnet test tests/XafPet.Desktop.Tests
+-   dotnet publish src/XafPet.Desktop/XafPet.Desktop.csproj -c Release -r win-x64
+- 
+
+### 文档
+- 桌面宠物实现计划 v1.0
+
+
 ## v1.0.1569 (2026-10-11)
 
 ### 新功能

+ 28 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1569
+> 当前版本: v1.0.1570
 
 ## 历史版本
 
@@ -8,6 +8,33 @@
 
 ---
 
+## v1.0.1570 (2026-10-11)
+
+### 新功能
+- 浠艾福桌面宠物 v1.0 完成(WPF + WebView2 架构)
+- 桌面宠物设计文档 v1.1 - 支持用户自定义状态
+- 桌面宠物设计文档 v1.0
+
+### 其他
+- - 架构:WPF (.NET 6) 主进程 + WebView2 渲染层,Bridge via cfcPetAPI
+- - 状态映射:14 种核心状态 + 用户自定义状态(custom_state_1~10)
+- - API 对接:POST /api/tasks/today、/api/energy/overview、/api/daily/checkin
+- - 形象配置:JSON Schema (PetConfig),支持浠宝/福宝预设 + 用户自定义 PNG
+- - 交互闭环:喝水提醒 → 点击确认 → API 打卡 → 庆祝动画
+- - 托盘常驻:最小化到托盘,右键菜单(设置/显示/退出),双击唤出
+- - Token 存储:Windows DPAPI 加密,%APPDATA%\XafPet\token.bin
+- - 动画库:20 个 CSS keyframes(idle/happy/busy/sad/celebrate 等)
+- - 测试:xUnit + Moq 单元测试 18 个(PetConfig/ApiClient/StateManager)
+- Windows 构建命令:
+-   dotnet build XafPet.Desktop.sln
+-   dotnet test tests/XafPet.Desktop.Tests
+-   dotnet publish src/XafPet.Desktop/XafPet.Desktop.csproj -c Release -r win-x64
+- 
+
+### 文档
+- 桌面宠物实现计划 v1.0
+
+
 ## v1.0.1569 (2026-10-11)
 
 ### 新功能