修复一些的过时测试 优化readme

This commit is contained in:
yml
2026-05-21 13:16:07 +08:00
parent 5474d77799
commit 25671078d6
38 changed files with 331 additions and 210 deletions
+218 -122
View File
@@ -1,156 +1,252 @@
# order-site-workspace # order_site
统一工作区版本的订单自动兑换系统。 订单自动兑换与履约管理系统。
项目采用前后端分离架构,主要用于接收订单、匹配履约规则、生成领取链接、管理库存、执行自动/半自动兑换,并提供后台页面处理人工复核、平台配置和任务追踪。
## 技术栈
- 前端:Vue 3、Vite、Element Plus、TypeScript
- 后端:Node.js、Express、PostgreSQL、Playwright
- OCR:独立 Python worker
- 网关:Caddy
- 本地与生产运行:Docker Compose
## 目录结构 ## 目录结构
```text ```text
apps/ apps/
backend/ Node + Express + Playwright + OCR worker backend/ 后端 API、任务编排、平台对接、数据库迁移
frontend/ Vue 3 + Vite + Element Plus src/
data/ 本地开发运行数据和平台配置
subservices/ocr-worker/ OCR 子服务源码
frontend/ 前端后台、领取页和配置页面
deploy/ deploy/
caddy/ Caddy 配置 caddy/ Caddy 反向代理配置
docker/ Dockerfile docker/ 前后端与 OCR 镜像 Dockerfile
docs/ 迁移与部署文档 docs/ 平台接口、迁移设计和部署资料
docker-compose.yml docker-compose.dev.yml 本地 Docker 开发环境
docker-compose.yml 生产/服务器 Compose
``` ```
## 本地开发 ## 本地开发
前端: 推荐直接使用 Docker,本地不需要额外维护 Node、PostgreSQL、Playwright 浏览器和 OCR 运行环境。
```bash
cd apps/frontend
npm install
npm run dev
```
后端:
```bash
cp .env.mac-docker.example .env
cd apps/backend/subservices/ocr-worker
uv sync
cd apps/backend
npm install
npm run dev
```
## Docker 部署
```bash
cp .env.server.example .env
mkdir -p deploy/data/backend
docker compose up -d
```
生产版 Compose 现在会把后端运行数据直接挂载到宿主机目录:
- `deploy/data/backend`
这样服务器上可以直接查看和备份:
- `logs/`
- `browser-sessions/`
- `redeem-screenshots/`
- `agiso-shops.json`
- `order-fulfillment-bindings.json`
如果你本地已经配好了店铺和履约绑定,首次上线前可以把这两份文件先放进去:
- `deploy/data/backend/agiso-shops.json`
- `deploy/data/backend/order-fulfillment-bindings.json`
后端启动时会自动执行数据库 migration、初始化默认管理员、同步履约目录;但 `/app/data` 下的业务配置文件仍以宿主机目录内容为准。
## Docker 本地开发
```bash ```bash
cp .env.mac-docker.example .env cp .env.mac-docker.example .env
docker compose -f docker-compose.dev.yml up -d --build docker compose -f docker-compose.dev.yml up -d --build
``` ```
开发环境现在不再把 Caddy 绑定到固定域名。 启动后常用入口:
这意味着:
- 本地直接访问 `http://localhost` 可用 - 前端、后台、领取页:`http://localhost`
- 用 ngrok / cloudflared 转发到 `http://localhost:80` 时,不需要因为随机子域名变化而改 Caddy 配置 - 后端健康检查:`http://localhost/health`
- 如果要生成发给用户的公网领取链接,只需要更新 `.env` 里的 `APP_BASE_URL` - 后端 API 前缀:`http://localhost/api/v1/...`
- OCR worker`http://127.0.0.1:8100/health`
- noVNC 浏览器画面:`http://127.0.0.1:6080/vnc.html`
开发版 Compose 现在是热更新模式 查看容器状态
- `postgres` 容器提供 PostgreSQL
- 后端源码挂载到容器里,运行 `npm run dev`
- 前端源码挂载到容器里,运行 `vite`
- Caddy 只负责把 `80/443` 反代到前后端容器
通常只有首次启动、改 Dockerfile、改系统依赖时才需要 `--build`
开发版 Compose 会把后端运行产物目录直接挂载到:
- [apps/backend/data](/Users/yml/codes/order-site-workspace/apps/backend/data)
这样本地可以直接看到:
- `data/logs/*.log`
- 浏览器会话产物
- 截图和证明文件
`TENCENT_BROWSER_HEADLESS=false` 时,开发版后端容器会自动启动 `Xvfb + x11vnc + noVNC`
默认可直接在本机访问:
- `http://127.0.0.1:6080/vnc.html`
这样即使整个项目都跑在 Docker 里,也可以直接查看后端 Playwright 浏览器画面。
PostgreSQL 数据则保存在 Docker volume 里:
- `postgres_dev_data`
当前 Dockerfile 已默认针对国内服务器优化以下下载源:
- Debian `apt` 使用腾讯云镜像
- `npm` 使用 `npmmirror`
- Python `pip` 使用腾讯云 PyPI 镜像
- 后端 Playwright 固定为 `1.42.1`,浏览器下载使用 `npmmirror`
如果你的服务器网络环境不同,也可以在构建时覆盖:
```bash ```bash
docker compose build \ docker compose -f docker-compose.dev.yml ps
--build-arg DEBIAN_MIRROR=mirrors.tuna.tsinghua.edu.cn \
--build-arg NPM_REGISTRY=https://registry.npmjs.org \
--build-arg UV_INDEX_URL=https://pypi.org/simple
``` ```
默认入口 查看日志
```bash
docker compose -f docker-compose.dev.yml logs -f backend
docker compose -f docker-compose.dev.yml logs -f frontend
docker compose -f docker-compose.dev.yml logs -f ocr-worker
```
停止环境:
```bash
docker compose -f docker-compose.dev.yml down
```
## 开发验证
本项目以容器内验证为准。
```bash
docker compose -f docker-compose.dev.yml exec -T backend npm test
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T frontend npm run lint
```
前端生产构建:
```bash
docker compose -f docker-compose.dev.yml exec -T frontend npm run build
```
后端数据库迁移通常会在启动时自动执行;需要手动执行时:
```bash
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate
```
## 热更新
开发版 Compose 会把源码挂载进容器:
- 修改 `apps/backend/src`:后端容器内 `node --watch` 自动重启
- 修改 `apps/frontend/src`Vite 自动 HMR
- 修改 `apps/backend/subservices/ocr-worker`:下次 OCR 调用会使用挂载后的代码
- 修改数据库 migration:重启后端会自动执行迁移
如果只改业务代码,一般不需要重新 `--build`。改 Dockerfile、系统依赖或基础镜像时再重新构建。
## 环境变量
本地开发从根目录 `.env` 读取,模板为:
```bash
cp .env.mac-docker.example .env
```
生产部署从 `.env.server.example` 复制:
```bash
cp .env.server.example .env
```
常用变量:
- `APP_BASE_URL`:应用公网地址,用于推导领取链接
- `CLAIM_BASE_URL`:领取页地址,优先级高于 `APP_BASE_URL`
- `ADMIN_SESSION_SECRET`:后台登录态签名密钥
- `ADMIN_DEFAULT_USERS_JSON`:默认后台用户
- `DATABASE_URL`PostgreSQL 连接串
- `TENCENT_BROWSER_HEADLESS`Playwright 是否无头运行
- `TENCENT_BROWSER_PREWARM`:启动时是否预热浏览器
- `OCR_BASE_URL`OCR worker 地址
## 运行数据
本地开发数据:
- 后端配置与运行产物:`apps/backend/data`
- PostgreSQL 数据:Docker volume `postgres_dev_data`
- 后端日志:`apps/backend/data/logs`
- 浏览器会话:`apps/backend/data/browser-sessions`
- 兑换截图:`apps/backend/data/redeem-screenshots`
生产部署数据:
- 后端数据挂载到 `deploy/data/backend`
- PostgreSQL 数据保存在 Docker volume `postgres_data`
- Caddy 数据保存在 Docker volume `caddy_data`
注意:`apps/backend/data/*.json` 中可能包含账号、Cookie、token、平台配置等敏感信息。提交代码前请确认没有把真实生产凭据提交到仓库。
## 生产部署
在服务器上准备 `.env` 后启动:
```bash
cp .env.server.example .env
mkdir -p deploy/data/backend
docker compose up -d --build
```
生产入口:
- 前端与领取页:`https://你的域名/` - 前端与领取页:`https://你的域名/`
- 后端健康检查:`https://你的域名/health` - 后端健康检查:`https://你的域名/health`
- 后端接口前缀`https://你的域名/api/v1/...` - 后端 API`https://你的域名/api/v1/...`
开发环境下的链接生成规则 生产 Compose 会自动
- 优先使用 `CLAIM_BASE_URL` - 启动 PostgreSQL
- 如果未设置 `CLAIM_BASE_URL`,后端会自动根据 `APP_BASE_URL` 推导为 `APP_BASE_URL/#/claim` - 启动 OCR worker
- 启动后端并执行 migration
- 初始化默认后台管理员
- 同步履约目录
- 通过 Caddy 暴露前端和 API
开发时如果只改业务代码,直接保留 `docker compose -f docker-compose.dev.yml up -d` 即可 查看生产日志
- 改后端 `src/`:容器内自动热重启 ```bash
- 改前端 `src/`Vite 自动热更新 docker compose logs -f backend
- 改 OCR Python:后端下次调用时直接走挂载后的最新源码 docker compose logs -f web
- 改数据库 schema:后端启动时会自动执行 migration ```
详细文档: ## 开发数据脚本
- [服务器部署教程](/Users/yml/codes/order-site-workspace/docs/%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B.md) 后端提供开发数据脚本:
- [首次上线检查清单](/Users/yml/codes/order-site-workspace/docs/%E9%A6%96%E6%AC%A1%E4%B8%8A%E7%BA%BF%E6%A3%80%E6%9F%A5%E6%B8%85%E5%8D%95.md)
- [mac 本地 Docker 调试模板](/Users/yml/codes/order-site-workspace/.env.mac-docker.example)
- [服务器部署模板](/Users/yml/codes/order-site-workspace/.env.server.example)
## 迁移原则 ```bash
docker compose -f docker-compose.dev.yml exec -T backend npm run seed:dev-data
docker compose -f docker-compose.dev.yml exec -T backend npm run cleanup:dev-data
```
- 不重写现有业务代码 脚本默认有预览/确认语义,执行前注意终端输出说明。
- 先统一目录、部署和配置
- 生产环境优先使用 `.env` 和宿主机数据目录挂载 ## 主要业务模块
- 本机 Docker 调试优先使用 [docker-compose.dev.yml](/Users/yml/codes/order-site-workspace/docker-compose.dev.yml)
- `apps/backend/src/routes`API 路由
- `apps/backend/src/services/order`:订单、webhook、库存和履约匹配
- `apps/backend/src/services/fulfillment`:快手 Cloud 等履约编排
- `apps/backend/src/services/session`:腾讯登录、二维码、兑换和截图
- `apps/backend/src/services/admin`:后台读写、权限、配置管理
- `apps/backend/src/services/platforms`:外部平台 API 封装
- `apps/backend/src/repositories`:数据库访问
- `apps/frontend/src/views/admin`:后台页面
- `apps/frontend/src/views/claim`:用户领取页
## 排障
如果页面打不开:
```bash
docker compose -f docker-compose.dev.yml ps
docker compose -f docker-compose.dev.yml logs -f web
```
如果后端未 ready
```bash
docker compose -f docker-compose.dev.yml logs -f backend
curl http://localhost/health/ready
```
如果浏览器兑换或扫码异常:
```bash
docker compose -f docker-compose.dev.yml logs -f backend
open http://127.0.0.1:6080/vnc.html
```
如果前端类型或 lint 失败:
```bash
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T frontend npm run lint
```
如果后端单测失败:
```bash
docker compose -f docker-compose.dev.yml exec -T backend npm test
```
## 提交前检查清单
```bash
docker compose -f docker-compose.dev.yml exec -T backend npm test
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T frontend npm run lint
```
确认项:
- 没有提交真实账号、Cookie、token、手机号或生产配置
- `.env` 未被提交
- 数据库 migration 与代码一起提交
- 领取链接相关改动已在 Docker 环境验证
- 涉及 Playwright/OCR 的改动已看过 noVNC 或相关日志
@@ -30,6 +30,7 @@ test('resolveAdminCloudtentaclesCredentialPayload merges payload and saved sourc
}, },
), ),
{ {
sourceKey: 'default',
baseUrl: 'https://override.example.com', baseUrl: 'https://override.example.com',
username: 'saved-user', username: 'saved-user',
password: 'saved-pass', password: 'saved-pass',
@@ -60,6 +61,7 @@ test('resolveAdminCloudtentaclesSessionPayload merges payload source and persist
}, },
), ),
{ {
sourceKey: 'default',
baseUrl: 'https://saved.example.com', baseUrl: 'https://saved.example.com',
token: 'payload-token', token: 'payload-token',
deviceId: 'saved-device', deviceId: 'saved-device',
@@ -111,6 +113,7 @@ test('buildAdminCloudtentaclesSessionSummary masks token and reports permission
}, },
), ),
{ {
sourceKey: 'default',
tokenMasked: 'abcdef****567890', tokenMasked: 'abcdef****567890',
permissionCount: 2, permissionCount: 2,
permissions: ['asset', 'sku'], permissions: ['asset', 'sku'],
@@ -138,6 +141,7 @@ test('buildAdminCloudtentaclesLoginResult assembles login response payload', ()
}, },
), ),
{ {
sourceKey: 'default',
baseUrl: 'https://cloud.example.com', baseUrl: 'https://cloud.example.com',
username: 'test-user', username: 'test-user',
phoneMasked: '138****5678', phoneMasked: '138****5678',
@@ -145,6 +149,7 @@ test('buildAdminCloudtentaclesLoginResult assembles login response payload', ()
responseMessage: 'success', responseMessage: 'success',
token: 'abcdef1234567890', token: 'abcdef1234567890',
session: { session: {
sourceKey: 'default',
tokenMasked: 'abcdef****567890', tokenMasked: 'abcdef****567890',
permissionCount: 1, permissionCount: 1,
permissions: ['asset'], permissions: ['asset'],
@@ -170,9 +175,11 @@ test('buildAdminCloudtentaclesValidateResult assembles validate response payload
{}, {},
), ),
{ {
sourceKey: 'default',
baseUrl: 'https://cloud.example.com', baseUrl: 'https://cloud.example.com',
loggedInAt: '2026-05-04T12:00:00.000Z', loggedInAt: '2026-05-04T12:00:00.000Z',
session: { session: {
sourceKey: 'default',
tokenMasked: 'abcdef****567890', tokenMasked: 'abcdef****567890',
permissionCount: 2, permissionCount: 2,
permissions: ['asset', 'sku'], permissions: ['asset', 'sku'],
@@ -88,6 +88,7 @@ test('resolveCloudtentaclesAdminContext merges payload source and persisted sess
) )
assert.deepEqual(context, { assert.deepEqual(context, {
sourceKey: 'default',
baseUrl: 'https://saved.example.com', baseUrl: 'https://saved.example.com',
token: 'token-1', token: 'token-1',
deviceId: 'device-saved', deviceId: 'device-saved',
@@ -113,6 +113,8 @@ test('normalizeAdminCloudtentaclesSourceConfigPayload applies defaults', () => {
deviceType: '2', deviceType: '2',
}), }),
{ {
key: 'default',
label: '',
enabled: false, enabled: false,
baseUrl: 'https://123.207.217.176', baseUrl: 'https://123.207.217.176',
username: 'user', username: 'user',
@@ -36,6 +36,7 @@ test('mapKuaishouCloudFulfillmentItemsToBindings maps 91kaquan rules', () => {
configId: '', configId: '',
cloudtentacles: { cloudtentacles: {
cloudSourceKey: 'default', cloudSourceKey: 'default',
cloudSourceKeyFallbacks: [],
skuId: 28, skuId: 28,
skuName: '', skuName: '',
vnKey: '1', vnKey: '1',
+40 -16
View File
@@ -1598,6 +1598,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.15.0", "version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
@@ -1659,13 +1671,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.14.0", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.16.0",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0" "proxy-from-env": "^2.1.0"
} }
}, },
@@ -1829,7 +1842,6 @@
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@@ -2378,9 +2390,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -2560,6 +2572,19 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3089,7 +3114,6 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/muggle-string": { "node_modules/muggle-string": {
@@ -3100,9 +3124,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -3290,9 +3314,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.8", "version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -3309,7 +3333,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.12",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -1,19 +1,19 @@
import { computed, ref, type Ref, type ComputedRef } from 'vue' import { computed, ref, type ComputedRef } from 'vue'
import { POLL_INTERVAL_MS, POLL_FAILURE_LIMIT } from './sessionConstants' import { POLL_INTERVAL_MS, POLL_FAILURE_LIMIT } from './sessionConstants'
export interface UseSessionPollingOptions<T> { export interface UseSessionPollingOptions {
/** Whether there is an active session used as a guard inside the poll loop. */ /** Whether there is an active session used as a guard inside the poll loop. */
hasSession: ComputedRef<boolean> hasSession: ComputedRef<boolean>
/** Called on each poll tick (typically refreshSessionSummary). */ /** Called on each poll tick (typically refreshSessionSummary). */
onPollTick: () => Promise<void> onPollTick: () => Promise<unknown>
/** Optional: determines whether polling should continue. Defaults to checking hasSession. */ /** Optional: determines whether polling should continue. Defaults to checking hasSession. */
shouldKeepPolling?: ComputedRef<boolean> shouldKeepPolling?: ComputedRef<boolean>
/** Context-specific warning message prefix when polling fails repeatedly. */ /** Context-specific warning message prefix when polling fails repeatedly. */
pollWarningPrefix: string pollWarningPrefix: string
} }
export function useSessionPolling(options: UseSessionPollingOptions<unknown>) { export function useSessionPolling(options: UseSessionPollingOptions) {
const { hasSession, onPollTick, pollWarningPrefix } = options const { hasSession, onPollTick, pollWarningPrefix } = options
const pollWarningMessage = ref('') const pollWarningMessage = ref('')
@@ -5,6 +5,8 @@ import type { TencentLoginType } from '@/types/tencent/session'
import { DEFAULT_LOGIN_TYPE } from './sessionConstants' import { DEFAULT_LOGIN_TYPE } from './sessionConstants'
type ReadableRef<T> = Pick<Ref<T>, 'value'>
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// syncLoginTypeFromDetail // syncLoginTypeFromDetail
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -102,7 +104,7 @@ export interface UseSessionPresentationOptions {
} }
export function useSessionRoleFacts(options: UseSessionPresentationOptions) { export function useSessionRoleFacts(options: UseSessionPresentationOptions) {
const { activityInfo, statusLabel, roleFactDefaults } = options const { activityInfo, roleFactDefaults } = options
const roleFacts = computed<FactItem[]>(() => [ const roleFacts = computed<FactItem[]>(() => [
{ {
@@ -168,17 +170,17 @@ export function useSessionResultFacts(options: UseSessionResultFactsOptions) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function buildRedeemBlockedReason(options: { export function buildRedeemBlockedReason(options: {
detail: ComputedRef<unknown | null> detail: ReadableRef<unknown | null>
hasSession: ComputedRef<boolean> hasSession: ReadableRef<boolean>
loginTypeLabel: ComputedRef<string> loginTypeLabel: ReadableRef<string>
redeemLoading: ComputedRef<boolean> redeemLoading: ReadableRef<boolean>
roleReady: ComputedRef<boolean> roleReady: ReadableRef<boolean>
roleConfirmed: ComputedRef<boolean> roleConfirmed: ReadableRef<boolean>
sessionNotice: ComputedRef<string> sessionNotice: ReadableRef<string>
context: 'claim' | 'admin' context: 'claim' | 'admin'
/** Claim-specific fields */ /** Claim-specific fields */
tokenStatus?: ComputedRef<string> tokenStatus?: ReadableRef<string>
requiresSupportReview?: ComputedRef<boolean> requiresSupportReview?: ReadableRef<boolean>
}): string { }): string {
const { const {
detail, detail,
@@ -1,4 +1,4 @@
import { computed, ref, watch, type Ref, type ComputedRef } from 'vue' import { computed, ref, watch, type ComputedRef } from 'vue'
/** /**
* Shared QR code display logic: renders the base64 QR image, tracks its * Shared QR code display logic: renders the base64 QR image, tracks its
@@ -122,7 +122,7 @@ export function useAdminManualRedeemPage() {
() => `请让客户使用${loginTypeLabel.value}扫码,并在手机上确认登录`, () => `请让客户使用${loginTypeLabel.value}扫码,并在手机上确认登录`,
) )
const { roleFacts } = useSessionRoleFacts({ useSessionRoleFacts({
activityInfo, activityInfo,
statusLabel, statusLabel,
roleFactDefaults: { nickname: '等待客户登录' }, roleFactDefaults: { nickname: '等待客户登录' },
@@ -230,10 +230,6 @@ export function useAdminManualRedeemPage() {
}) })
// ── Helpers ──────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────
function applyLoginTypeFromDetail(nextDetail: AdminManualRedeemDetail) {
loginType.value = syncLoginTypeFromDetail(nextDetail, loginType.value)
}
function restartPollingIfNeeded(nextDetail = detail.value) { function restartPollingIfNeeded(nextDetail = detail.value) {
resetPolling() resetPolling()
@@ -88,7 +88,7 @@ export function useClaimPage(token: string) {
() => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`, () => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`,
) )
const { roleFacts } = useSessionRoleFacts({ useSessionRoleFacts({
activityInfo, activityInfo,
statusLabel, statusLabel,
roleFactDefaults: { nickname: '等待扫码登录' }, roleFactDefaults: { nickname: '等待扫码登录' },
@@ -1,8 +1,6 @@
import { apiDelete, apiGet, apiPost } from '@/lib/http' import { apiDelete, apiGet, apiPost } from '@/lib/http'
import type { import type {
AdminCloudtentaclesLoginTestResult, AdminCloudtentaclesLoginTestResult,
AdminCloudtentaclesPersistedSession,
AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSkuListResult, AdminCloudtentaclesSkuListResult,
AdminCloudtentaclesSendSmsResult, AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSourceConfigResponse, AdminCloudtentaclesSourceConfigResponse,
+1
View File
@@ -81,6 +81,7 @@ export type {
AdminCloudtentaclesSessionsMap, AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSourceConfig, AdminCloudtentaclesSourceConfig,
AdminCloudtentaclesPersistedSession, AdminCloudtentaclesPersistedSession,
AdminCloudtentaclesSessionItem,
AdminCloudtentaclesSendSmsResult, AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSessionSummary, AdminCloudtentaclesSessionSummary,
AdminCloudtentaclesLoginTestResult, AdminCloudtentaclesLoginTestResult,
@@ -26,6 +26,8 @@ export interface AdminCloudtentaclesPersistedSession {
hasToken: boolean hasToken: boolean
} }
export type AdminCloudtentaclesSessionItem = AdminCloudtentaclesPersistedSession
export interface AdminCloudtentaclesSessionsMap { export interface AdminCloudtentaclesSessionsMap {
[sourceKey: string]: AdminCloudtentaclesPersistedSession [sourceKey: string]: AdminCloudtentaclesPersistedSession
} }
@@ -39,6 +39,7 @@ export type {
AdminCloudtentaclesSessionsMap, AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSourceConfig, AdminCloudtentaclesSourceConfig,
AdminCloudtentaclesPersistedSession, AdminCloudtentaclesPersistedSession,
AdminCloudtentaclesSessionItem,
AdminCloudtentaclesSendSmsResult, AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSessionSummary, AdminCloudtentaclesSessionSummary,
AdminCloudtentaclesLoginTestResult, AdminCloudtentaclesLoginTestResult,
@@ -1,4 +1,4 @@
import type { AdminFulfillmentBindingConfigItem, AdminObservedProductItem } from '@/types/admin' import type { AdminObservedProductItem } from '@/types/admin'
export type EditableBinding = { export type EditableBinding = {
id: string id: string
@@ -3,7 +3,6 @@ import { reactive, ref } from 'vue'
import { lookupAdminFulfillmentBindingOrder } from '@/services/admin' import { lookupAdminFulfillmentBindingOrder } from '@/services/admin'
import type { AdminFulfillmentLookupItem, AdminFulfillmentLookupResult } from '@/types/admin' import type { AdminFulfillmentLookupItem, AdminFulfillmentLookupResult } from '@/types/admin'
import type { ImportableProductCandidate } from './types'
import type { useFulfillmentBindings } from './useFulfillmentBindings' import type { useFulfillmentBindings } from './useFulfillmentBindings'
export function useFulfillmentBindingsLookup( export function useFulfillmentBindingsLookup(
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { AdminNinetyoneOrderItem } from '@/types/admin' import type { AdminNinetyoneOrderItem } from '@/types/admin'
const props = defineProps<{ defineProps<{
ninetyoneLookupLoading: boolean ninetyoneLookupLoading: boolean
ninetyoneLookupErrorMessage: string ninetyoneLookupErrorMessage: string
ninetyoneLookupResults: AdminNinetyoneOrderItem[] ninetyoneLookupResults: AdminNinetyoneOrderItem[]
@@ -68,9 +68,12 @@ const emit = defineEmits<{
</div> </div>
<div class="section-action-group"> <div class="section-action-group">
<el-button round @click="emit('expandAll')">全部展开</el-button> <el-button round @click="emit('expandAll')">全部展开</el-button>
<el-checkbox :model-value="enabled" @update:model-value="emit('update:enabled', $event)" <el-checkbox
>启用整条配置</el-checkbox :model-value="enabled"
@update:model-value="emit('update:enabled', Boolean($event))"
> >
启用整条配置
</el-checkbox>
<el-button round @click="emit('collapseReady')">收起已完成</el-button> <el-button round @click="emit('collapseReady')">收起已完成</el-button>
<el-button round :loading="cloudSkuCatalogLoading" @click="emit('refreshCloudSku')" <el-button round :loading="cloudSkuCatalogLoading" @click="emit('refreshCloudSku')"
>刷新 cloud SKU</el-button >刷新 cloud SKU</el-button
@@ -528,6 +528,7 @@ export function useKuaishouCloudConfig() {
// consume shops // consume shops
getKuaishouConsumeShopOptions, getKuaishouConsumeShopOptions,
findKuaishouConsumeShop, findKuaishouConsumeShop,
getDefaultKuaishouConsumeShop,
formatKuaishouConsumeShopOption, formatKuaishouConsumeShopOption,
handleKuaishouConsumeShopSelected, handleKuaishouConsumeShopSelected,
// collapse // collapse
@@ -1,6 +1,6 @@
import { computed, nextTick, reactive, ref } from 'vue' import { computed, reactive, ref } from 'vue'
import { showError, showSuccess } from '@/lib/feedback' import { showSuccess } from '@/lib/feedback'
import { fetchAdminNinetyoneOrders } from '@/services/admin' import { fetchAdminNinetyoneOrders } from '@/services/admin'
import type { AdminNinetyoneOrderItem } from '@/types/admin' import type { AdminNinetyoneOrderItem } from '@/types/admin'
@@ -39,9 +39,7 @@ export function useKuaishouCloudNinetyone(
function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableItem { function createItemFromNinetyoneOrder(item: AdminNinetyoneOrderItem): EditableItem {
const productNo = String(item.productNo || '').trim() const productNo = String(item.productNo || '').trim()
const productName = String(item.productName || productNo).trim() const productName = String(item.productName || productNo).trim()
const defaultShop = config.getDefaultKuaishouConsumeShop ? config.findKuaishouConsumeShop() : null const shop = config.getDefaultKuaishouConsumeShop()
const shopOpts = config.getKuaishouConsumeShopOptions()
const shop = defaultShop || (shopOpts.length > 0 ? shopOpts[0] : null)
return { return {
localId: crypto.randomUUID(), localId: crypto.randomUUID(),
id: '', id: '',
@@ -32,7 +32,6 @@ const {
singleDisplayValue, singleDisplayValue,
bulkText, bulkText,
skuSuggestionsLoading, skuSuggestionsLoading,
inventorySkuSuggestions,
entrySkuSuggestions, entrySkuSuggestions,
// actions // actions
loadInventoryItems, loadInventoryItems,
@@ -49,8 +48,6 @@ const {
getSkuNamingHint, getSkuNamingHint,
formatInventoryStatus, formatInventoryStatus,
getGroupSuggestion, getGroupSuggestion,
// utils
hasAdminRole,
formatAdminDateTime, formatAdminDateTime,
} = useAdminInventory() } = useAdminInventory()
</script> </script>
@@ -6,15 +6,12 @@ import {
fetchAdminInventoryItems, fetchAdminInventoryItems,
fetchAdminInventorySkuSuggestions, fetchAdminInventorySkuSuggestions,
importAdminInventoryItems, importAdminInventoryItems,
invalidateAdminInventoryItem,
releaseAdminInventoryItem,
} from '@/services/admin' } from '@/services/admin'
import type { import type {
AdminInventoryItemListItem, AdminInventoryItemListItem,
AdminInventorySkuSuggestion, AdminInventorySkuSuggestion,
AdminPagination, AdminPagination,
} from '@/types/admin' } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time' import { formatAdminDateTime } from '@/utils/admin-time'
import { import {
adminInventoryCredentialTypeOptions, adminInventoryCredentialTypeOptions,
@@ -425,7 +422,6 @@ export function useAdminInventory() {
formatInventoryStatus, formatInventoryStatus,
getGroupSuggestion, getGroupSuggestion,
// utils // utils
hasAdminRole,
formatAdminDateTime, formatAdminDateTime,
adminInventoryCredentialTypeOptions, adminInventoryCredentialTypeOptions,
adminInventoryStatusOptions, adminInventoryStatusOptions,
@@ -184,6 +184,7 @@ const taskDetailLink = computed(() =>
:review-card-title="reviewCardTitle" :review-card-title="reviewCardTitle"
:review-ready="reviewReady" :review-ready="reviewReady"
:result-ready="resultReady" :result-ready="resultReady"
:screenshot-empty-title="screenshotEmptyTitle"
:screenshot-empty-message="screenshotEmptyMessage" :screenshot-empty-message="screenshotEmptyMessage"
:screenshot-url="screenshotUrl" :screenshot-url="screenshotUrl"
:screenshot-loading="screenshotLoading" :screenshot-loading="screenshotLoading"
@@ -3,6 +3,7 @@ type Props = {
reviewCardTitle: string reviewCardTitle: string
reviewReady: boolean reviewReady: boolean
resultReady: boolean resultReady: boolean
screenshotEmptyTitle: string
screenshotEmptyMessage: string screenshotEmptyMessage: string
screenshotUrl: string screenshotUrl: string
screenshotLoading: boolean screenshotLoading: boolean
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
type TaskDetail = { type TaskDetail = {
taskNo?: string taskNo?: string
taskId?: string taskId?: string | number
status?: string status?: string
} }
@@ -50,7 +50,7 @@ type Props = {
removeSource: (sourceKey: string) => void removeSource: (sourceKey: string) => void
} }
const props = defineProps<Props>() defineProps<Props>()
</script> </script>
<template> <template>
@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { import type {
AdminCloudtentaclesPersistedSession, AdminCloudtentaclesPersistedSession,
AdminCloudtentaclesSessionItem,
AdminCloudtentaclesSessionsMap, AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSourceItem,
} from '@/types/admin' } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time' import { formatAdminDateTime } from '@/utils/admin-time'
@@ -15,7 +15,7 @@ type Props = {
selectedSourceKey: string selectedSourceKey: string
} }
const props = defineProps<Props>() defineProps<Props>()
</script> </script>
<template> <template>
@@ -3,8 +3,8 @@ import type {
AdminCloudtentaclesLoginTestResult, AdminCloudtentaclesLoginTestResult,
AdminCloudtentaclesPersistedSession, AdminCloudtentaclesPersistedSession,
AdminCloudtentaclesSendSmsResult, AdminCloudtentaclesSendSmsResult,
AdminCloudtentaclesSessionItem,
AdminCloudtentaclesSessionsMap, AdminCloudtentaclesSessionsMap,
AdminCloudtentaclesSourceItem,
AdminCloudtentaclesValidateSessionResult, AdminCloudtentaclesValidateSessionResult,
} from '@/types/admin' } from '@/types/admin'
import AdminCloudtentaclesInfoCard from './AdminCloudtentaclesInfoCard.vue' import AdminCloudtentaclesInfoCard from './AdminCloudtentaclesInfoCard.vue'
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { EditableKuaishouEticketShop } from '../composables/types' import type { EditableKuaishouEticketShop } from '../../composables/types'
import type { import type {
AdminKuaishouEticketConsumeResult, AdminKuaishouEticketConsumeResult,
AdminKuaishouEticketDetailResult, AdminKuaishouEticketDetailResult,
@@ -195,7 +195,7 @@ function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop {
id: crypto.randomUUID(), id: crypto.randomUUID(),
shopId: item.shopId, shopId: item.shopId,
shopName: item.shopName, shopName: item.shopName,
accessToken: item.accessToken, accessToken: item.AccessToken,
enabled: item.enabled !== false, enabled: item.enabled !== false,
messageTemplate: item.messageTemplate, messageTemplate: item.messageTemplate,
autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate, autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate,
@@ -1,4 +1,4 @@
import { ref, computed, type Ref } from 'vue' import { ref, computed } from 'vue'
import { showError, showSuccess } from '@/lib/feedback' import { showError, showSuccess } from '@/lib/feedback'
import { import {
consumeAdminKuaishouEticket, consumeAdminKuaishouEticket,
@@ -66,13 +66,10 @@ export function useAdminKuaishouEticketPlatform() {
function hydrateKuaishouEticketConfig(data: { function hydrateKuaishouEticketConfig(data: {
filePath: string filePath: string
source: { source: AdminKuaishouEticketSourceConfig
baseUrl?: string
shops: AdminKuaishouEticketShopInfoResult['shop'][]
}
}) { }) {
kuaishouEticketFilePath.value = data.filePath kuaishouEticketFilePath.value = data.filePath
shopManager.setShopsFromConfig(data.source.shops as any) shopManager.setShopsFromConfig(data.source.shops)
kuaishouEticketForm.value = { kuaishouEticketForm.value = {
baseUrl: data.source.baseUrl || 'https://s.kwaixiaodian.com', baseUrl: data.source.baseUrl || 'https://s.kwaixiaodian.com',
selectedShopId: '', selectedShopId: '',
@@ -106,7 +103,7 @@ export function useAdminKuaishouEticketPlatform() {
} }
function addKuaishouEticketShop() { function addKuaishouEticketShop() {
const { newSelectedId } = shopManager.addKuaishouEticketShop(kuaishouEticketForm.value.selectedShopId) const { newSelectedId } = shopManager.addKuaishouEticketShop()
setKuaishouEticketDebugShop(newSelectedId) setKuaishouEticketDebugShop(newSelectedId)
} }
@@ -1,12 +1,12 @@
import { ref } from 'vue' import { ref } from 'vue'
import type { EditableKuaishouEticketShop } from '../types' import type { EditableKuaishouEticketShop } from './types'
import type { AdminKuaishouEticketShopConfigItem } from '@/types/admin' import type { AdminKuaishouEticketShopConfigItem } from '@/types/admin'
export function useKuaishouEticketShopManager() { export function useKuaishouEticketShopManager() {
const kuaishouEticketShops = ref<EditableKuaishouEticketShop[]>([]) const kuaishouEticketShops = ref<EditableKuaishouEticketShop[]>([])
const expandedKuaishouEticketShopIds = ref<string[]>([]) const expandedKuaishouEticketShopIds = ref<string[]>([])
function addKuaishouEticketShop(selectedShopId: string) { function addKuaishouEticketShop() {
const shop = createEmptyKuaishouEticketShop() const shop = createEmptyKuaishouEticketShop()
kuaishouEticketShops.value.unshift(shop) kuaishouEticketShops.value.unshift(shop)
return { shop, newSelectedId: shop.id } return { shop, newSelectedId: shop.id }
@@ -12,7 +12,7 @@ import {
regenerateAdminTaskClaimLink, regenerateAdminTaskClaimLink,
retryAdminTask, retryAdminTask,
} from "@/services/admin"; } from "@/services/admin";
import type { AdminTaskActionResponse, AdminTaskListItem } from "@/types/admin"; import type { AdminTaskListItem } from "@/types/admin";
import { hasAdminRole } from "@/utils/admin-auth"; import { hasAdminRole } from "@/utils/admin-auth";
import { import {
formatBindingRoleSummary, formatBindingRoleSummary,
@@ -379,7 +379,7 @@ function resetFilters() {
action: async () => { action: async () => {
const response = await retryAdminTask(row.taskId); const response = await retryAdminTask(row.taskId);
if (response.data.claimUrl) if (response.data.claimUrl)
lastClaimUrl.value = response.data.claimUrl; lastClaimUrl = response.data.claimUrl;
}, },
successMessage: '任务已重试', successMessage: '任务已重试',
confirmText: `确认重试任务 ${row.taskNo} 吗?`, confirmText: `确认重试任务 ${row.taskNo} 吗?`,
@@ -8,7 +8,7 @@ interface Props {
canCloseTasks: boolean canCloseTasks: boolean
} }
const props = defineProps<Props>() defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
retry: [] retry: []
@@ -1,18 +1,11 @@
import { computed, onBeforeUnmount, ref } from 'vue' import { computed, onBeforeUnmount, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { showError, showSuccess } from '@/lib/feedback' import { showConfirm, showError, showSuccess } from '@/lib/feedback'
import { import {
closeAdminTask,
completeAdminTaskManualDispatch, completeAdminTaskManualDispatch,
fetchAdminTaskScreenshot, fetchAdminTaskScreenshot,
fetchAdminTaskDetail, fetchAdminTaskDetail,
markAdminTaskManualReview,
redeemAdminTaskAssisted,
regenerateAdminTaskClaimLink,
releaseAdminTaskInventoryBinding,
retryAdminTask,
confirmAdminTaskAssistedRole,
} from '@/services/admin' } from '@/services/admin'
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin' import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth' import { hasAdminRole } from '@/utils/admin-auth'
@@ -52,7 +45,11 @@ export function useAdminTaskDetail() {
const claimLinkInvalid = computed(() => { const claimLinkInvalid = computed(() => {
const tokenStatus = String(detail.value?.claimToken?.status || '').trim() const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
return Boolean(detail.value?.claimToken?.claimUrl) && tokenStatus && tokenStatus !== 'active' return Boolean(
detail.value?.claimToken?.claimUrl &&
tokenStatus &&
tokenStatus !== 'active',
)
}) })
const screenshotSectionTitle = computed(() => { const screenshotSectionTitle = computed(() => {
@@ -6,7 +6,7 @@ import {
refreshAdminTaskKuaishouCloudRoleInfo, refreshAdminTaskKuaishouCloudRoleInfo,
returnNumberAdminTaskKuaishouCloudFulfillment, returnNumberAdminTaskKuaishouCloudFulfillment,
} from '@/services/admin' } from '@/services/admin'
import type { AdminTaskDetail } from '@/types/admin' import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
import { formatKuaishouRoleInfoLabel } from '@/utils/admin-display' import { formatKuaishouRoleInfoLabel } from '@/utils/admin-display'
import { formatAdminDateTime } from '@/utils/admin-time' import { formatAdminDateTime } from '@/utils/admin-time'
@@ -15,7 +15,7 @@ import type { ChecklistItem, RoleInfoEntry, SummaryCard } from './types'
export function useAdminTaskKuaishouCloud( export function useAdminTaskKuaishouCloud(
detail: Ref<AdminTaskDetail | null>, detail: Ref<AdminTaskDetail | null>,
runAction: ( runAction: (
action: () => Promise<{ data: any }>, action: () => Promise<{ data: AdminTaskActionResponse }>,
successMessage: string, successMessage: string,
confirmText: string, confirmText: string,
) => Promise<void>, ) => Promise<void>,
@@ -61,7 +61,7 @@ const claim = useKuaishouCloudClaim(() => props.token)
:refreshing-role="claim.refreshingRole.value" :refreshing-role="claim.refreshingRole.value"
:confirming-role="claim.confirmingRole.value" :confirming-role="claim.confirmingRole.value"
:flow="claim.flow.value" :flow="claim.flow.value"
:format-date-time="claim.formatDateTime" :format-date-time="claim.formatAdminDateTime"
@open-bind-url="claim.openBindUrl($event)" @open-bind-url="claim.openBindUrl($event)"
@refresh-role="claim.refreshRole()" @refresh-role="claim.refreshRole()"
@confirm-role="claim.confirmRole()" @confirm-role="claim.confirmRole()"
@@ -85,7 +85,7 @@ const claim = useKuaishouCloudClaim(() => props.token)
:order-item="claim.orderItem.value" :order-item="claim.orderItem.value"
:role-name="claim.roleName.value" :role-name="claim.roleName.value"
:role-id="claim.roleId.value" :role-id="claim.roleId.value"
:format-date-time="claim.formatDateTime" :format-date-time="claim.formatAdminDateTime"
/> />
</template> </template>