diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json
new file mode 100644
index 0000000..2b429f0
--- /dev/null
+++ b/.workbuddy/expert-history.json
@@ -0,0 +1,17 @@
+{
+ "version": 2,
+ "sessions": {
+ "ebf5e3bab92e4319aec2c39116541728": [
+ {
+ "expertId": "BackendArchitect",
+ "name": "磐石石",
+ "profession": "后端架构师",
+ "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/BackendArchitect/BackendArchitect.png",
+ "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/BackendArchitect/BackendArchitect_zh.md",
+ "usedAt": 1776327849643,
+ "industryId": "all"
+ }
+ ]
+ },
+ "lastUpdated": 1776336333635
+}
\ No newline at end of file
diff --git a/.workbuddy/memory/2026-04-16.md b/.workbuddy/memory/2026-04-16.md
new file mode 100644
index 0000000..ae05f9d
--- /dev/null
+++ b/.workbuddy/memory/2026-04-16.md
@@ -0,0 +1,76 @@
+# 2026-04-16 工作日志
+
+## OPS2 系统工作流程分析
+
+完成了对 OPS2 流程管理系统三端架构的全面分析,生成了完整的工作流程分析报告(ops2_workflow_analysis.md)。
+
+### 系统技术栈
+- 后端:Go + Gin + GORM,支持 SQLite/MySQL/PostgreSQL 可配置切换
+- PC前端:Vue3 + Vite + Pinia + Axios
+- 移动端:uni-app(Vue3+Vite),目标 H5 + Android
+
+### 核心功能模块
+1. **用户认证**:Cookie-based 认证,MD5+salt 密码哈希,支持 Remember Me
+2. **日程管理**:日历事件 CRUD,权限分层(创建者/模块管理员/全局管理员)
+3. **采购订单**:6种状态流转(pending→ordered→arrived→received/lost/returned),含完整审计日志
+4. **文件系统**:SHA256 内容去重,MIME 白名单校验,图片上传服务
+
+---
+
+## 移动端重构(对标 PC 前端)
+
+完成了 ops_uniapp 移动端的全面重构,参考 PC 前端架构对齐。
+
+### 新增/重构文件
+- `api/request.js`:统一请求封装(自动注入 cookie、统一 err_code 解析、cookie 过期自动跳登录)
+- `api/auth.js`、`api/schedule.js`、`api/purchase.js`:对标 PC 端 API 模块
+- `store/user.js`:轻量单例 Store(对标 PC 端 Pinia useUserStore,含 restoreSession)
+- `pages/signin.vue`:重构登录页(表单验证、密码显示/隐藏、remember me)
+- `pages/index/index.vue`:重构首页(今日日程卡片 + 待处理采购统计)
+- `pages/setting/my_info.vue`:重构设置页(头像上传、信息修改、退出登录)
+- `pages.json`:修正 API 路径前缀(/api/v1 → /api),更新页面路由
+- `App.vue`:接入 userStore.restoreSession()
+
+### 关键设计
+- API base 统一为 `/api`(修复旧版 /api/v1 错误)
+- cookie 认证流程与 PC 端完全对齐
+- store/user.js 用模块单例替代 Pinia(uni-app 兼容)
+
+---
+
+## 补建预留路由对应的 Vue 文件
+
+修复了 `vite:import-analysis` 报错(pages.json 声明了路由但文件不存在)。
+
+### 新建文件
+- `pages/schedule/schedule.vue`:移动端日程月份视图,支持按月浏览、添加/编辑/删除日程、颜色分类,30s 轮询刷新
+- `pages/purchase/list.vue`:移动端采购订单列表,支持搜索/状态过滤/分页加载更多;底部抽屉查看订单详情(费用明细/图片/变更记录);已登录可变更订单状态
+
+### 重要发现
+- 移动端 `my_network_func.js` 中 `head_path = "/api/v1"` 与后端实际 `/api` 路径不匹配,需修复
+- 审计日志系统完善(TabScheduleLog、TabPurchaseLog、TabPurchaseCommit)
+- 后端同时托管静态前端产物,单体部署架构
+
+---
+
+## @api 命名空间重构(修复 404 模块请求)
+
+### 问题
+`GET http://localhost:5173/api/auth.js net::ERR_ABORTED 404`
+
+manifest.json 配置了 `/api` 代理到后端 8080,导致所有 `/api` 开头的请求都被代理。
+即使加了 vite.config.js 的 resolve.alias,HBuilderX 的内置 dev server 行为也不完全符合预期。
+
+### 解决
+将 API 模块目录从 `api/` 重命名为 `@api/`,彻底消除与 `/api` 代理前缀的冲突。
+
+- `api/` → `@api/`,所有 import 路径相应更新
+- 真正的后端 API 请求走 `/api`(代理到 8080)
+- `@api/*.js` 模块不走 `/api` 代理,不会冲突
+- `manifest.json` 的 h5.devServer 只保留 disableHostCheck,proxy 移至 vite.config.js
+
+### 更新文件
+- `@api/request.js`、`@api/auth.js`、`@api/schedule.js`、`@api/purchase.js`(新建)
+- `store/user.js`、`pages/signin.vue`、`pages/setting/my_info.vue`、`pages/index/index.vue`、`pages/schedule/schedule.vue`、`pages/purchase/list.vue`(更新 import 路径)
+- `vite.config.js`(简化 alias 配置)
+- 删除旧 `api/` 目录
diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/ops_uniapp/@api/auth.js b/frontend/ops_uniapp/@api/auth.js
new file mode 100644
index 0000000..0ec05dd
--- /dev/null
+++ b/frontend/ops_uniapp/@api/auth.js
@@ -0,0 +1,42 @@
+/**
+ * 认证相关 API
+ * 对标 PC 前端 src/api/auth.js
+ */
+import { request } from './request.js'
+
+export const authApi = {
+ /** 登录 */
+ login(username, password, remember = false) {
+ return request.post('/users/login', { username, password, remember })
+ },
+
+ /** 注册 */
+ register(username, email, password) {
+ return request.post('/users/register', { username, useremail: email, userpass: password })
+ },
+
+ /** 通过 cookie 获取用户信息 */
+ getUserInfo() {
+ return request.post('/users/getinfo', {})
+ },
+
+ /** 修改密码 */
+ changePassword(oldPass, newPass) {
+ return request.post('/users/changePassword', { oldpass: oldPass, newpass: newPass })
+ },
+
+ /** 修改邮箱 */
+ changeEmail(newEmail) {
+ return request.post('/users/changeEmail', { newemail: newEmail })
+ },
+
+ /** 修改用户信息 */
+ updateInfo(data) {
+ return request.post('/users/updateInfo', data)
+ },
+
+ /** 更新头像(文件上传) */
+ updateAvatar(filePath) {
+ return request.upload('/users/updateAvatar', filePath)
+ },
+}
diff --git a/frontend/ops_uniapp/@api/purchase.js b/frontend/ops_uniapp/@api/purchase.js
new file mode 100644
index 0000000..68de33e
--- /dev/null
+++ b/frontend/ops_uniapp/@api/purchase.js
@@ -0,0 +1,42 @@
+/**
+ * 采购订单相关 API
+ * 对标 PC 前端 src/api/purchase.js
+ */
+import { request } from './request.js'
+
+export const purchaseApi = {
+ /** 获取订单列表(分页+搜索+状态过滤) */
+ getOrders(params = {}) {
+ return request.post('/purchase/getorders', params)
+ },
+
+ /** 获取单个订单详情 */
+ getOrder(id) {
+ return request.post('/purchase/getorder', { id })
+ },
+
+ /** 获取各状态订单数量统计 */
+ getOrderCount() {
+ return request.post('/purchase/getordercount', {})
+ },
+
+ /** 新增订单 */
+ addOrder(data) {
+ return request.post('/purchase/addorder', data)
+ },
+
+ /** 更新订单 */
+ updateOrder(data) {
+ return request.post('/purchase/updateorder', data)
+ },
+
+ /** 更新订单状态 */
+ updateStatus(data) {
+ return request.post('/purchase/updatestatus', data)
+ },
+
+ /** 删除订单 */
+ deleteOrder(id) {
+ return request.post('/purchase/deleteorder', { id })
+ },
+}
diff --git a/frontend/ops_uniapp/@api/request.js b/frontend/ops_uniapp/@api/request.js
new file mode 100644
index 0000000..587db9f
--- /dev/null
+++ b/frontend/ops_uniapp/@api/request.js
@@ -0,0 +1,97 @@
+/**
+ * 统一网络请求封装
+ * 对标 PC 前端 src/api/index.js 的设计
+ * - 自动注入 cookie
+ * - 统一解析 err_code / return 字段
+ * - Cookie 过期自动清理并跳转登录
+ */
+
+import { userStore } from '../store/user.js'
+
+const API_BASE = '/api'
+
+/**
+ * 底层 POST 请求
+ * @param {string} path - 接口路径,如 /users/login
+ * @param {object} data - 业务数据(会被包在 data 字段下)
+ * @returns {Promise<{errCode, data, raw}>}
+ */
+function post(path, data = {}) {
+ const body = { data }
+
+ // 自动注入 cookie
+ const cookieValue = userStore.getCookieValue()
+ if (cookieValue) {
+ body.userCookieValue = cookieValue
+ }
+
+ return new Promise((resolve, reject) => {
+ uni.request({
+ url: API_BASE + path,
+ method: 'POST',
+ header: { 'Content-Type': 'application/json' },
+ data: body,
+ timeout: 15000,
+ success(res) {
+ const raw = res.data
+ const errCode = raw?.err_code ?? -1
+
+ // Cookie 过期(err_code === -44),自动登出并跳转登录
+ if (errCode === -44) {
+ userStore.logout()
+ uni.reLaunch({ url: '/pages/signin' })
+ reject(new Error('Cookie expired'))
+ return
+ }
+
+ resolve({
+ errCode,
+ data: raw?.return ?? null,
+ raw,
+ })
+ },
+ fail(err) {
+ uni.showToast({ title: '网络连接失败', icon: 'none' })
+ reject(err)
+ },
+ })
+ })
+}
+
+/**
+ * 上传文件(FormData)
+ * @param {string} path - 接口路径
+ * @param {string} filePath - 本地文件路径(uni.chooseImage 返回的 tempFilePaths[0])
+ * @param {string} name - 文件字段名,默认 'file'
+ */
+function upload(path, filePath, name = 'file') {
+ const cookieValue = userStore.getCookieValue()
+
+ return new Promise((resolve, reject) => {
+ uni.uploadFile({
+ url: API_BASE + path,
+ filePath,
+ name,
+ formData: cookieValue ? { cookie: cookieValue } : {},
+ timeout: 30000,
+ success(res) {
+ try {
+ const raw = JSON.parse(res.data)
+ resolve({
+ errCode: raw?.err_code ?? -1,
+ data: raw?.return ?? null,
+ raw,
+ })
+ } catch {
+ reject(new Error('JSON parse error'))
+ }
+ },
+ fail(err) {
+ uni.showToast({ title: '上传失败', icon: 'none' })
+ reject(err)
+ },
+ })
+ })
+}
+
+export const request = { post, upload }
diff --git a/frontend/ops_uniapp/@api/schedule.js b/frontend/ops_uniapp/@api/schedule.js
new file mode 100644
index 0000000..89bbff0
--- /dev/null
+++ b/frontend/ops_uniapp/@api/schedule.js
@@ -0,0 +1,27 @@
+/**
+ * 日程相关 API
+ * 对标 PC 前端 src/api/schedule.js
+ */
+import { request } from './request.js'
+
+export const scheduleApi = {
+ /** 获取日程列表 */
+ getEvents(params = {}) {
+ return request.post('/schedule/getevents', params)
+ },
+
+ /** 新增日程 */
+ addEvent(data) {
+ return request.post('/schedule/addevent', data)
+ },
+
+ /** 编辑日程 */
+ editEvent(data) {
+ return request.post('/schedule/editevent', data)
+ },
+
+ /** 删除日程 */
+ deleEvent(data) {
+ return request.post('/schedule/deleevent', data)
+ },
+}
diff --git a/frontend/ops_uniapp/App.vue b/frontend/ops_uniapp/App.vue
index 6fc6d96..f61f9c5 100644
--- a/frontend/ops_uniapp/App.vue
+++ b/frontend/ops_uniapp/App.vue
@@ -1,17 +1,14 @@
diff --git a/frontend/ops_uniapp/manifest.json b/frontend/ops_uniapp/manifest.json
index 771779a..49fc04e 100644
--- a/frontend/ops_uniapp/manifest.json
+++ b/frontend/ops_uniapp/manifest.json
@@ -71,18 +71,7 @@
"vueVersion" : "3",
"h5" : {
"devServer" : {
- "disableHostCheck" : true,
- "proxy" : {
- "/api" : {
- "target" : "http://127.0.0.1:8080",
- "changeOrigin" : true,
- "secure" : false,
- "ws": false,
- "pathRewrite" : {
- "^/api" : ""
- }
- }
- }
+ "disableHostCheck" : true
}
}
diff --git a/frontend/ops_uniapp/pages.json b/frontend/ops_uniapp/pages.json
index ed73c64..a597c7d 100644
--- a/frontend/ops_uniapp/pages.json
+++ b/frontend/ops_uniapp/pages.json
@@ -1,46 +1,47 @@
{
- "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
- {
- "path": "pages/index/index",
- "style": {
- "navigationBarTitleText": "uni-app"
- }
- },
- {
- "path" : "pages/test/test",
- "style" :
- {
- "navigationBarTitleText" : ""
- }
- },
- {
- "path" : "pages/setting/my_info",
- "style" :
- {
- "navigationBarTitleText" : ""
- }
- },
- {
- "path" : "pages/signin",
- "style" :
- {
- "navigationBarTitleText" : ""
- }
- },
- {
- "path" : "components/setting-menu/setting-menu",
- "style" :
- {
- "navigationBarTitleText" : ""
- }
- }
- ],
- "globalStyle": {
- "navigationBarTextStyle": "black",
- "navigationBarTitleText": "uni-app",
- "navigationBarBackgroundColor": "#F8F8F8",
- "backgroundColor": "#F8F8F8",
- "navigationStyle": "custom"
- },
- "uniIdRouter": {}
+ "pages": [
+ {
+ "path": "pages/index/index",
+ "style": {
+ "navigationBarTitleText": "Operations",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/signin",
+ "style": {
+ "navigationBarTitleText": "登录",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/setting/my_info",
+ "style": {
+ "navigationBarTitleText": "个人设置",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/schedule/schedule",
+ "style": {
+ "navigationBarTitleText": "日程",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/purchase/list",
+ "style": {
+ "navigationBarTitleText": "采购订单",
+ "navigationStyle": "custom"
+ }
+ }
+ ],
+ "globalStyle": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "Operations",
+ "navigationBarBackgroundColor": "#FFFFFF",
+ "backgroundColor": "#F3F4F6",
+ "navigationStyle": "custom"
+ },
+ "uniIdRouter": {}
}
diff --git a/frontend/ops_uniapp/pages/index/index.vue b/frontend/ops_uniapp/pages/index/index.vue
index 134d8ca..078ccc2 100644
--- a/frontend/ops_uniapp/pages/index/index.vue
+++ b/frontend/ops_uniapp/pages/index/index.vue
@@ -1,55 +1,484 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+ {{ isLoggedIn ? `你好,${displayName}` : '欢迎使用' }}
+
+ {{ todayDisplay }}
+
+
+
+
+
+
+
+
+ 加载中…
+
+
+
+
+
+
+
+ {{ getColorLabel(item.BgColor) }}
+
+
+
+ {{ item.Title }}
+
+ {{ formatDateRange(item.StartDate, item.EndDate) }}
+
+
+
+
+
+
+
+ 今日暂无日程
+
+
+
+ 查看全部 ›
+
+
+
+
+
+
+
+
+
+
+ {{ loadingOrders ? '…' : pendingOrderCount || '—' }}
+
+ 待处理
+
+
+
+ 登录后查看
+
+
+
+ 查看全部 ›
+
+
+
+
+
+
+
+
diff --git a/frontend/ops_uniapp/pages/purchase/list.vue b/frontend/ops_uniapp/pages/purchase/list.vue
new file mode 100644
index 0000000..06b663c
--- /dev/null
+++ b/frontend/ops_uniapp/pages/purchase/list.vue
@@ -0,0 +1,827 @@
+
+
+
+
+
+
+
+ 🔍
+
+ ✕
+
+
+
+
+
+
+
+ {{ opt.label }}
+
+ {{ countMap[opt.value] }}
+
+
+
+
+
+
+
+ 加载中…
+
+
+
+
+ 📦
+ 暂无采购订单
+
+
+
+
+
+
+ {{ order.Title }}
+
+ {{ getStatusLabel(order.OrderStatus) }}
+
+
+
+ #{{ order.ID }}
+ ·
+ {{ formatDate(order.CreatedAt) }}
+
+
+
+
+ 加载更多…
+
+
+ 已加载全部 {{ total }} 条
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载中…
+
+
+
+
+
+
+ 名称
+ {{ detailOrder.Title }}
+
+
+ 当前状态
+
+ {{ getStatusLabel(detailOrder.OrderStatus) }}
+
+
+
+ 款式备注
+ {{ detailOrder.Styles }}
+
+
+ 备注
+ {{ detailOrder.Remark }}
+
+
+ 链接
+ uni.showToast({ title: '链接已复制', icon: 'none' }) })">
+ {{ detailOrder.Link }}(点击复制)
+
+
+
+
+
+
+ 变更状态
+
+
+ {{ opt.label }}
+
+
+
+
+
+
+ 费用明细
+
+ {{ costTypeMap[c.CostType] || c.CostType }}
+ x{{ c.Quantity }}
+ {{ formatPrice(c.Price) }}
+ {{ currencyMap[c.CurrencyType] || '-' }}
+
+
+
+ 合计
+
+
+ {{ g.currency }} {{ g.total }}
+
+
+
+
+
+
+
+ 图片备注
+
+ getPhotoUrl(ph.Sha256)), current: getPhotoUrl(p.Sha256) })"
+ />
+
+
+
+
+
+ 变更记录
+
+
+
+
+ {{ getStatusLabel(c.status) }}
+ {{ formatDate(c.createdAt) }}
+
+
+
+ getPhotoUrl(h)), current: getPhotoUrl(hash) })"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 目标状态
+
+ {{ getStatusLabel(pendingStatus) }}
+
+
+
+ 变更备注(可选)
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/ops_uniapp/pages/schedule/schedule.vue b/frontend/ops_uniapp/pages/schedule/schedule.vue
new file mode 100644
index 0000000..3a7a3cb
--- /dev/null
+++ b/frontend/ops_uniapp/pages/schedule/schedule.vue
@@ -0,0 +1,627 @@
+
+
+
+
+
+
+
+ ‹
+
+
+ {{ curYear }} 年 {{ curMonth }} 月
+ 点击回今天
+
+
+ ›
+
+
+
+
+
+ 加载中…
+
+
+
+
+ 📅
+ 本月暂无日程
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.Title }}
+
+ {{ colorLabel(item.BgColor) }}
+
+ · {{ formatDate(item.StartDate) }} ~ {{ formatDate(item.EndDate) }}
+
+
+
+ ›
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ 日程内容
+
+
+
+
+
+ 开始日期
+ form.startDate = e.detail.value">
+
+ {{ form.startDate || '请选择' }}
+ ›
+
+
+
+
+
+
+ 结束日期
+ form.endDate = e.detail.value">
+
+ {{ form.endDate || '请选择' }}
+ ›
+
+
+
+
+
+
+ 类型
+
+
+
+ {{ c.label }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/ops_uniapp/pages/setting/my_info.vue b/frontend/ops_uniapp/pages/setting/my_info.vue
index 62aca7f..0657d76 100644
--- a/frontend/ops_uniapp/pages/setting/my_info.vue
+++ b/frontend/ops_uniapp/pages/setting/my_info.vue
@@ -1,417 +1,465 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
信息设置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+/* ── 危险区 ── */
+.danger-card {
+ background: #fff;
+ border-radius: 24rpx;
+ padding: 36rpx;
+ margin-bottom: 40rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+.logout-btn {
+ width: 100%;
+ height: 96rpx;
+ background: #fee2e2;
+ color: #dc2626;
+ border-radius: 16rpx;
+ font-size: 32rpx;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+}
+
diff --git a/frontend/ops_uniapp/pages/signin.vue b/frontend/ops_uniapp/pages/signin.vue
index 3faf453..6503cf9 100644
--- a/frontend/ops_uniapp/pages/signin.vue
+++ b/frontend/ops_uniapp/pages/signin.vue
@@ -1,192 +1,297 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
登录你的账号
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 还没账号?
-
点击注册
-
- 忘记密码?
-
-
-
-
-
-
-

-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+ 密码
+
+
+
+ {{ showPassword ? '👁' : '🙈' }}
+
+
+ {{ errors.password }}
+
+
+
+
+
+ ✓
+
+ 在此设备上保持登录
+
+
+
+
+
+
+
+
+
diff --git a/frontend/ops_uniapp/store/user.js b/frontend/ops_uniapp/store/user.js
new file mode 100644
index 0000000..f38bd19
--- /dev/null
+++ b/frontend/ops_uniapp/store/user.js
@@ -0,0 +1,136 @@
+/**
+ * 用户状态管理(轻量 Store,对标 PC 前端 src/stores/user.js)
+ *
+ * uni-app 不支持 Pinia,这里用模块化单例替代,
+ * 提供与 PC 前端完全一致的接口语义。
+ */
+
+import { authApi } from '../@api/auth.js'
+
+const STORAGE_KEY_COOKIE = 'userCookie'
+const STORAGE_KEY_USER = 'userData'
+const STORAGE_KEY_INFO = 'userInfo'
+
+// ── 内部状态 ──────────────────────────────────────────────
+const _state = {
+ user: null, // TabUser_ 基本信息
+ userInfo: null, // TabUserInfo_ 详情
+ userCookie: null, // Cookie 对象
+ isLoggedIn: false,
+}
+
+// ── 辅助方法 ──────────────────────────────────────────────
+function _loadJson(key) {
+ try {
+ const raw = uni.getStorageSync(key)
+ return raw ? JSON.parse(raw) : null
+ } catch { return null }
+}
+
+function _saveJson(key, data) {
+ uni.setStorageSync(key, JSON.stringify(data))
+}
+
+function _remove(key) {
+ uni.removeStorageSync(key)
+}
+
+// ── 对外接口(与 PC 端 useUserStore 语义一致) ─────────────
+export const userStore = {
+
+ // ── Getters ──
+
+ get isLoggedIn() { return _state.isLoggedIn },
+ get user() { return _state.user },
+ get userInfo() { return _state.userInfo },
+ get userCookie() { return _state.userCookie },
+
+ /** 获取 Cookie Value 字符串(供 request.js 自动注入) */
+ getCookieValue() {
+ return _state.userCookie?.Value ?? ''
+ },
+
+ /** 头像 URL */
+ getAvatarUrl() {
+ if (_state.userInfo?.AvatarPath) {
+ return `/api/static/avatar/${_state.userInfo.AvatarPath}`
+ }
+ return '/static/ava.svg'
+ },
+
+ /** 生日(YYYY-MM-DD) */
+ getBirthday() {
+ if (!_state.userInfo?.Birthdate) return ''
+ return String(_state.userInfo.Birthdate).substring(0, 10)
+ },
+
+ /** 用户名(显示名 > 账号名) */
+ getDisplayName() {
+ return _state.userInfo?.Username || _state.user?.Name || ''
+ },
+
+ // ── Actions ──
+
+ /**
+ * 登录成功后调用,保存 cookie 并拉取用户信息
+ * @param {object} cookie - 后端返回的 Cookie 对象
+ */
+ login(cookie) {
+ _state.userCookie = cookie
+ _state.isLoggedIn = true
+ _saveJson(STORAGE_KEY_COOKIE, cookie)
+ // 验证是否过期
+ if (cookie.ExpiresAt && new Date(cookie.ExpiresAt) < new Date()) {
+ this.logout()
+ return
+ }
+ this.fetchUserInfo()
+ },
+
+ /** 登出,清理所有本地状态 */
+ logout() {
+ _state.user = null
+ _state.userInfo = null
+ _state.userCookie = null
+ _state.isLoggedIn = false
+ _remove(STORAGE_KEY_COOKIE)
+ _remove(STORAGE_KEY_USER)
+ _remove(STORAGE_KEY_INFO)
+ },
+
+ /** 拉取用户信息并缓存 */
+ async fetchUserInfo() {
+ try {
+ const { errCode, data } = await authApi.getUserInfo()
+ if (errCode === 0) {
+ _state.user = data?.user ?? null
+ _state.userInfo = data?.userInfo ?? null
+ if (_state.user) _saveJson(STORAGE_KEY_USER, _state.user)
+ if (_state.userInfo) _saveJson(STORAGE_KEY_INFO, _state.userInfo)
+ }
+ } catch { /* request.js 已处理提示 */ }
+ },
+
+ /**
+ * 应用启动时恢复登录状态(在 App.vue 的 onLaunch 中调用)
+ */
+ restoreSession() {
+ const cookie = _loadJson(STORAGE_KEY_COOKIE)
+ if (cookie) {
+ // 验证 cookie 是否还在有效期
+ if (cookie.ExpiresAt && new Date(cookie.ExpiresAt) < new Date()) {
+ this.logout()
+ return
+ }
+ _state.userCookie = cookie
+ _state.isLoggedIn = true
+ // 恢复缓存的用户信息(避免冷启动白屏)
+ _state.user = _loadJson(STORAGE_KEY_USER)
+ _state.userInfo = _loadJson(STORAGE_KEY_INFO)
+ // 后台静默刷新
+ this.fetchUserInfo()
+ } else {
+ this.logout()
+ }
+ },
+}
diff --git a/frontend/ops_uniapp/vite.config.js b/frontend/ops_uniapp/vite.config.js
new file mode 100644
index 0000000..91403e7
--- /dev/null
+++ b/frontend/ops_uniapp/vite.config.js
@@ -0,0 +1,37 @@
+/**
+ * uni-app + Vue3 Vite 配置
+ *
+ * 解决:API 模块目录命名为 @api/,避免与 /api 代理前缀冲突。
+ * 真正的后端 API 请求走 /api(代理到 8080)。
+ */
+import { defineConfig } from 'vite'
+import uni from '@dcloudio/vite-plugin-uni'
+import path from 'path'
+
+export default defineConfig({
+ plugins: [uni()],
+
+ resolve: {
+ alias: [
+ // @api/* → 本地 @api/ 目录(存放 API 模块文件)
+ { find: /^@api\/(.*)/, replacement: path.resolve(__dirname, '@api/$1') },
+ ],
+ },
+
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://127.0.0.1:8080',
+ changeOrigin: true,
+ secure: false,
+ ws: false,
+ bypass(req) {
+ // .js 等模块文件不走代理(@api/ 模块不带 /api 前缀,不会走到这里)
+ if (req.url && /\.\w+$/.test(req.url)) {
+ return false
+ }
+ },
+ },
+ },
+ },
+})