Compare commits

...

93 Commits

Author SHA1 Message Date
df5cfc5eba feat: 修改请求时机
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-02-27 15:02:08 +08:00
8100459c4e feat: dressfor页面&生成outfit逻辑修改
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-02-27 13:44:19 +08:00
10ee247b8d bugfix: 图片引入
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-01-29 14:23:57 +08:00
7f34ce80b9 feat: 测试部署
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-01-29 14:04:32 +08:00
57359d1067 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-01-29 13:13:04 +08:00
9101116430 chore: 更换设计师图片 2026-01-29 13:11:36 +08:00
X1627315083
7a87c6cd11 Merge branches 'main' and 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-01-19 13:40:54 +08:00
X1627315083
c8dc6cf8d1 对话页面指定标识字体加粗 2026-01-19 13:39:51 +08:00
f092a76162 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-01-14 09:49:25 +08:00
48a32a60a1 bugfix: customer页面选择顾客列表 2026-01-14 09:41:51 +08:00
X1627315083
f157c6ead3 调整设计师名字
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-01-13 16:09:16 +08:00
X1627315083
592792d071 fix
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-01-13 14:48:56 +08:00
X1627315083
aace73d5c4 修改设计师名字最后两个改为sera、edi
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-01-13 14:39:38 +08:00
X1627315083
02dcfba4ba fix
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2026-01-07 11:48:31 +08:00
X1627315083
465fa5e6ae fix
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-31 10:12:46 +08:00
X1627315083
0ec9e4dc46 调整outfit流程
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-31 10:07:08 +08:00
X1627315083
e230b4c83f Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 15:57:20 +08:00
X1627315083
cf15e371ab 修改product页面标题颜色 2025-12-30 15:53:27 +08:00
李志鹏
ef314931eb 1
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 15:36:49 +08:00
李志鹏
71a3946aae 1
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 15:35:49 +08:00
李志鹏
38f027d3ad Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 14:58:22 +08:00
李志鹏
d497471e09 调整 2025-12-30 14:58:15 +08:00
92c571b5f9 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 14:51:39 +08:00
6156c88429 style: 样式修改 2025-12-30 14:51:19 +08:00
X1627315083
319130dc6a 调整product的history标签位置
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 14:18:10 +08:00
李志鹏
6a2b456bf3 selectStyle
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 13:31:45 +08:00
X1627315083
135adb3907 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 13:10:53 +08:00
089a266c35 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 13:05:10 +08:00
a9732a659e feat: 对话加载动画 2025-12-30 11:54:13 +08:00
X1627315083
24e221902e 反推新增参数 2025-12-30 11:40:39 +08:00
李志鹏
8b92d360c9 Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 11:25:37 +08:00
李志鹏
f10039e9dc 1 2025-12-30 11:25:30 +08:00
9d0b29ab95 feat: 切换用户之后返回主页
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 11:16:48 +08:00
433dec935a Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 10:14:01 +08:00
dfdfce2076 bugfix: 聊天界面菜单&customer界面间距 2025-12-30 10:12:25 +08:00
李志鹏
eebe7b5b40 Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-30 10:03:02 +08:00
李志鹏
fc1c212eca 添加分享按钮 2025-12-30 10:02:55 +08:00
X1627315083
9f338a772d 修复魔改页面无法跳转到outfit页面 2025-12-30 09:45:26 +08:00
8e50f3d8a5 style: continue按钮背景样式
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 16:53:44 +08:00
f5bfad7973 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 16:46:24 +08:00
27bb0394f8 style: continue按钮背景 2025-12-29 16:46:09 +08:00
李志鹏
73ae6744ab Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 16:35:02 +08:00
李志鹏
2b42ef6ab7 Outfit收藏 2025-12-29 16:34:55 +08:00
X1627315083
0794749f9a Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 16:04:35 +08:00
X1627315083
5f84eb496c 调整不同流程下style生成数量 2025-12-29 16:04:29 +08:00
10fc7f3e30 style: dressfor页面样式
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 15:47:37 +08:00
6b19672f1c style: re-try字体颜色
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 15:37:54 +08:00
32245b8931 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 15:35:54 +08:00
25852e7c5a style: 文字颜色 2025-12-29 15:35:38 +08:00
X1627315083
5d89d06531 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 15:25:53 +08:00
X1627315083
279776c4f7 调整product不同流程的标题 2025-12-29 15:25:25 +08:00
李志鹏
94cb354483 Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 15:21:04 +08:00
李志鹏
88abe91538 customize更改历史图标 2025-12-29 15:20:58 +08:00
X1627315083
55ed8474be fix
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 15:20:03 +08:00
李志鹏
fab733c7c7 fix
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 14:52:24 +08:00
李志鹏
3826e553b1 Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 14:50:33 +08:00
李志鹏
8cb77593f8 更改viewtype方式 2025-12-29 14:48:54 +08:00
X1627315083
891fb3f4b0 fix
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 14:43:28 +08:00
X1627315083
c2b800eb35 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 14:43:03 +08:00
X1627315083
15a4220fbc 选择风格变为生成四个,调整渐变按钮样式 2025-12-29 14:42:50 +08:00
9da2cd9dc2 chore: retry图标
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 13:04:26 +08:00
8af7092f56 style: 选择顾客弹窗
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 11:24:01 +08:00
f0ff9ebcbe Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 10:51:09 +08:00
9c41bf4711 style: 样式修改.
bugfix: customer选择页面弹窗触发错误.
2025-12-29 10:46:00 +08:00
李志鹏
59c864fef3 更改字体
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-29 10:20:46 +08:00
李志鹏
28f67cf228 Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-24 12:06:49 +08:00
李志鹏
1be879fbe4 修改账号信息 2025-12-24 12:04:11 +08:00
8c2f4ca6e6 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-24 11:50:04 +08:00
53b9a83b80 feat: 右上角图标随个人中心弹窗展示而变化 2025-12-24 11:48:31 +08:00
李志鹏
9dd3bed850 fix
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-24 11:34:10 +08:00
a5cf919ded Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-24 10:38:46 +08:00
42a7194156 bugfix: 文案错误 2025-12-24 10:36:54 +08:00
李志鹏
b206cae025 1
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-24 10:09:25 +08:00
李志鹏
1a52eaa3c3 Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 17:29:56 +08:00
李志鹏
70cf624d88 按钮样式 2025-12-23 17:29:49 +08:00
a629a08e66 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 16:51:39 +08:00
73e8662895 style: 文案&样式修改 2025-12-23 16:51:23 +08:00
X1627315083
9a51259b11 修改style生成文案
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 16:31:30 +08:00
X1627315083
e6d542fb78 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 16:29:46 +08:00
33659049ce Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 15:52:35 +08:00
9310761a5d style: 文案和样式修改 2025-12-23 15:52:20 +08:00
李志鹏
321c0bdb92 Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 15:10:56 +08:00
李志鹏
30e46dff15 添加下拉加载分页 2025-12-23 15:10:50 +08:00
X1627315083
6b255728af 调整loding动画细节 2025-12-23 15:05:15 +08:00
8e73c90746 bugfix: 聊天界面白屏
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 14:45:23 +08:00
65617baa96 feat: 切换customer
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 14:41:09 +08:00
e8a72ec0aa Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 14:22:02 +08:00
c356b1b503 Merge branch 'main' of http://18.167.251.121:10003/aidlab/lanecarford_front 2025-12-23 14:21:21 +08:00
X1627315083
1b7694cee0 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 14:20:11 +08:00
b17637b2bc feat: 获取顾客列表&新建顾客 2025-12-23 14:19:00 +08:00
X1627315083
665194f18e tryon 反推style 2025-12-23 14:00:30 +08:00
李志鹏
c6a9d91159 fix
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-23 09:30:33 +08:00
李志鹏
51336fff77 添加历史流程数据状态
All checks were successful
git提交控制 AiDA WEB-Node.js main 分支构建部署 / build (20.19.0) (push) Has been skipped
2025-12-22 16:34:43 +08:00
62 changed files with 2149 additions and 928 deletions

View File

@@ -59,3 +59,19 @@ export const googleAuth = (data: GoogleAuthParamsType): Promise<LoginResponse> =
params: data
})
}
/** 更改用户信息
* @param data 包含用户信息的对象
* @param data.username 用户名
* @param data.email 邮箱
* @param data.password 密码
* @returns 包含更新后的用户信息的对象
*/
export const updateUserInfo = (data: any) => {
return request({
url: '/api/auth/updateUserInfo',
method: 'post',
data
})
}

View File

@@ -8,26 +8,26 @@ import request from '@/utils/request'
* @param data.stylist 提示词
* @param data.gender 原始试穿效果id
* @param data.num 是否重新生成 0-否1-是
*/
*/
export function generateRequestOutfit(data: Object) {
return request({
url: '/api/style/requestOutfit',
method: 'post',
data,
})
return request({
url: '/api/style/requestOutfit',
method: 'post',
data
})
}
/**
* 获取传达风格
* @param data 获取传达风格数据
* @param data.requestIDs 获取生成结果的taskId
*/
*/
export function getRequestOutfit(data: Object) {
return request({
url: '/api/style/getOutfitResult',
method: 'get',
params:data
})
return request({
url: '/api/style/getOutfitResult',
method: 'get',
params: data
})
}
/**
@@ -41,13 +41,13 @@ export function getRequestOutfit(data: Object) {
* @param data.prompt 提示词
* @param data.originalTryOnId 原始试穿效果id
* @param data.isRegenerated 是否重新生成 0-否1-是
*/
*/
export function generateTryOnEffect(data: Object) {
return request({
url: '/api/try-on-effects/generate',
method: 'post',
data,
})
return request({
url: '/api/try-on-effects/generate',
method: 'post',
data
})
}
/**
* 生成试穿效果-演示
@@ -55,13 +55,13 @@ export function generateTryOnEffect(data: Object) {
* @param data.customerPhotoId 顾客照片id
* @param data.prompt 提示词
* @param data.tryonUrl AI魔改url
*/
*/
export function generateTryOnEffectDemo(data: Object) {
return request({
url: '/api/try-on-effects/reFace',
method: 'post',
data,
})
return request({
url: '/api/try-on-effects/reFace',
method: 'post',
data
})
}
/** 上传图片-AI换脸
@@ -71,56 +71,111 @@ export function generateTryOnEffectDemo(data: Object) {
* @param data.file 顾客照片文件
*/
export function uploadCustomerPhoto(data: FormData) {
return request({
url: '/api/customer-photos/upload',
method: 'post',
data,
loading: true,
})
return request({
url: '/api/customer-photos/upload',
method: 'post',
data,
loading: true
})
}
/**
* 设置喜欢
* @param tryOnId 试穿效果id
*/
*/
export function setTryOnEffectFavorite(tryOnId: string | number) {
if (!tryOnId) return Promise.reject('试穿效果id不能为空');
return request({
url: `/api/try-on-effects/set-favorite/${tryOnId}`,
method: 'post',
})
if (!tryOnId) return Promise.reject('试穿效果id不能为空')
return request({
url: `/api/try-on-effects/set-favorite/${tryOnId}`,
method: 'post'
})
}
/**
* 取消喜欢
* @param tryOnId 试穿效果id
*/
*/
export function cancelTryOnEffectFavorite(tryOnId: string | number) {
if (!tryOnId) return Promise.reject('试穿效果id不能为空');
return request({
url: `/api/try-on-effects/cancel-favorite/${tryOnId}`,
method: 'post',
})
if (!tryOnId) return Promise.reject('试穿效果id不能为空')
return request({
url: `/api/try-on-effects/cancel-favorite/${tryOnId}`,
method: 'post'
})
}
/** 查询进店记录-library
* @param customerId 客户id
*/
export function getCustomerPhotos(customerId: string | number) {
if (!customerId) return Promise.reject('客户id不能为空');
return request({
url: `/api/visit-records/customer/${customerId}`,
method: 'get',
})
if (!customerId) return Promise.reject('客户id不能为空')
return request({
url: `/api/visit-records/customer/${customerId}`,
method: 'get'
})
}
/** 删除进店记录-library
* @param visitRecordId 进店记录id
*/
export function deleteCustomerPhoto(visitRecordId: string | number) {
if (!visitRecordId) return Promise.reject('进店记录id不能为空');
if (!visitRecordId) return Promise.reject('进店记录id不能为空')
return request({
url: `/api/visit-records/${visitRecordId}`,
method: 'delete'
// loading: true,
})
}
/** 查询收藏列表
* @param visitRecordId 进店记录id
*/
export function getTryOnEffectFavoriteList(visitRecordId: string | number) {
if (!visitRecordId) return Promise.reject('进店记录id不能为空')
return request({
url: `/api/try-on-effects/favorites/${visitRecordId}`,
method: 'get'
})
}
/** 查询某套试穿效果列表
* @param styleId 服装id
*/
export function getTryOnEffectStyleList(styleId: string | number) {
if (!styleId) return Promise.reject('服装id不能为空')
return request({
url: `/api/try-on-effects/style/${styleId}`,
method: 'get'
})
}
// 获取当前Sales名下的customers列表
export interface CustomerListParams {
current: number
size: number
desc: boolean
sortField?: 'createTime'
sortOrder?: 'DESC' | 'ASC'
keyword?: string
startTime?: string
endTime?: string
status?: number
offset?: number
limit?: number
}
export const getCustomerList = (data: CustomerListParams) => {
return request({
url: '/api/customers/getAllCustomer',
method: 'post',
data
})
}
// 创建顾客
export interface CreateCustomerParams {
nickname: string
vipId: string
}
export const createCustomer = (data: CreateCustomerParams) => {
return request({
url: `/api/visit-records/${visitRecordId}`,
method: 'delete',
// loading: true,
})
url: '/api/customers/createCustomer',
method: 'get',
params: data
})
}
// /** 查询收藏列表
@@ -162,19 +217,19 @@ export function getGenerateHistoricals(params: Object) {
interface CustomerInfo {
vipId: string
}
export const customerCheckin = (data: CustomerInfo) => {
return request({
url: '/api/customers/checkIn',
method: 'get',
params: data,
})
export const customerCheckin = (data: CustomerInfo) => {
return request({
url: '/api/customers/checkIn',
method: 'get',
params: data
})
}
// AI对话
interface AIConversation {
message: string
sessionId: string | number //用户ID
gender: 'male' | 'female' //性别
message: string
sessionId: string | number //用户ID
gender: 'male' | 'female' //性别
}
export const streamChatAddress = '/api/llm/streamChat'
@@ -185,11 +240,46 @@ export const streamChatAddress = '/api/llm/streamChat'
* @param data.visitRecordId 进店记录id
* @param data.customerId 顾客id
* @param data.suggestion 意见和建议
*/
*/
export function addTryOnEffectComment(data: Object) {
return request({
url: '/api/try-on-effects/add-comment',
method: 'post',
data,
})
}
}
/**
* like outfit接口
* @param styleId 设置like的styleid
*/
export function setStyleFavorite(styleId: Object) {
return request({
url: `/api/style/set-favorite/${styleId}`,
method: 'post',
})
}
/**
* 取消like outfit接口
* @param styleId 取消like的styleid
*/
export function cancelStyleFavorite(styleId: Object) {
return request({
url: `/api/style/cancel-favorite/${styleId}`,
method: 'post',
})
}
/**
* try on 返推outfitId
* @param tryOnEffectsId tryOnId
*/
export function retrieveAndRegenerate(data: Object) {
return request({
url: `/api/style/retrieveAndRegenerate`,
method: 'get',
params: data,
})
}

View File

@@ -1,6 +1,8 @@
button.sandblasted-blurred {
box-sizing: content-box;
border: 0.4rem solid #fff;
width: 35rem;
height: 8.3rem;
box-sizing: border-box;
border: 0.25rem solid #fff;
font-family: satoshiMedium;
font-weight: 500;
font-size: 4rem;

View File

@@ -1,6 +1,8 @@
button.sandblasted-blurred {
box-sizing: content-box;
border: 0.4rem solid #fff;
width: 35rem;
height: 8.3rem;
box-sizing: border-box;
border: 0.25rem solid #fff;
font-family: satoshiMedium;
font-weight: 500;
font-size: 4rem;
@@ -41,7 +43,9 @@ button.sandblasted-blurred {
//只使用浅色模式
@media (prefers-color-scheme: dark) {
:root, body {
:root,
body {
background: white !important;
color: black !important;
}
@@ -71,10 +75,11 @@ html:root {
--van-toast-default-width: 88rem;
}
.van-toast{
.van-toast {
min-height: fit-content;
max-width: none;
}
.van-toast__text {
font-size: 4rem;
height: 5rem;
@@ -84,21 +89,23 @@ html:root {
}
button.general,
.general_button{
.general_button {
border-radius: .7rem;
border: 3px solid #000;
background-color: #000;
text-align: center;
color: #fff;
font-family: satoshiMedium;
&.smail{
&.smail {
font-size: 3.6rem;
width: 24.6rem;
line-height: 6.7rem;
}
&.big{
&.big {
font-size: 3.8rem;
line-height: 7.4rem;
width: 3.4rem;
}
}
}

View File

@@ -1,4 +1,4 @@
<svg width="34" height="33" viewBox="0 0 34 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.96387 16.8115C1.96387 19.7782 2.8436 22.6783 4.49182 25.1451C6.14004 27.6118 8.48272 29.5344 11.2236 30.6697C13.9645 31.805 16.9805 32.1021 19.8902 31.5233C22.7999 30.9445 25.4727 29.5159 27.5705 27.4181C29.6682 25.3203 31.0969 22.6476 31.6756 19.7379C32.2544 16.8282 31.9574 13.8122 30.8221 11.0713C29.6867 8.33038 27.7641 5.9877 25.2974 4.33948C22.8307 2.69126 19.9306 1.81152 16.9639 1.81152C12.7705 1.8273 8.74548 3.46356 5.73053 6.37819L1.96387 10.1449" stroke="black" stroke-width="2.28788" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.96387 1.81152V10.1449H10.2972M16.9639 8.47819V16.8115L23.6305 20.1448" stroke="black" stroke-width="2.28788" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 21C1 24.9556 2.17298 28.8224 4.37061 32.1114C6.56823 35.4004 9.69181 37.9638 13.3463 39.4776C17.0008 40.9913 21.0222 41.3874 24.9018 40.6157C28.7814 39.844 32.3451 37.9392 35.1421 35.1421C37.9392 32.3451 39.844 28.7814 40.6157 24.9018C41.3874 21.0222 40.9913 17.0008 39.4776 13.3463C37.9638 9.69181 35.4004 6.56823 32.1114 4.37061C28.8224 2.17298 24.9556 1 21 1C15.4088 1.02103 10.0422 3.20272 6.02222 7.08889L1 12.1111" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 1V12.1111H12.1111M21 9.88889V21L29.8889 25.4444" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 831 B

After

Width:  |  Height:  |  Size: 759 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 860.38">
<path d="M0,778.02v-31c5.85-.83,5.72-6.77,7.59-9.91,8.69-14.56,35.82-36.92,48.91-50.09,132.41-133.33,265.22-266.72,399-399,24.48-24.21,50.22-64.36,90.41-43.4,11.6,6.05,52.53,47.35,62.07,58.93,36.58,44.38-10.81,75.6-38.98,103.98-134.47,135.49-269.91,271.36-406,406-16.26,16.09-36.44,43.61-60.47,46.53-29.22,3.55-48.2-22.96-66.04-41.03-9.24-9.36-21.12-18.28-28.34-29.66-2.17-3.42-2.85-10.44-8.15-11.35ZM411.35,365.37c-2.17,2.21-15.35,15.62-15.83,16.67-.89,1.93-.9,4.04,0,5.97,13.11,12.89,26.45,25.54,39.5,38.49,9.43,9.36,18.54,18.95,28.02,27.98,3.81,3.63,7.06,10.05,13.42,10.57l111.05-110.01c8.1-9.65,12.08-18.21,5.33-30.36-3.97-7.14-45.89-48.98-53.82-55.18-15.22-11.91-27.19-3.82-38.92,7.13-29.56,27.59-60.23,59.7-88.75,88.75ZM45.35,731.37c-11.89,12.23-27.69,25.33-17.21,44.01,4.11,7.33,43.34,46.29,51.36,52.64,12.24,9.71,22.88,9.89,35.03.02l338.5-338.5c2.02-1.93,3.87-4.47,2.24-7.24l-76.71-77.33c-6.81-3.73-7.97,2-11.53,5.57-106.81,107.19-216.25,212.35-321.68,320.82Z"/>
<path d="M880,334.02v17c-5.37-.47-7.34,5.21-11.28,7.22-8.66,4.42-23.83,6.25-34.09,10.91-26.75,12.14-29.6,29.14-37.9,54.1-3.19,9.58-7.95,22.23-20.19,21.83-17.04-.55-18.67-24.85-23.04-37.04-6.38-17.79-17.31-31.49-34.79-39.21-12.9-5.7-50.73-9.57-44.24-30.86,4.1-13.47,23.9-15.02,35.08-18.92,23.2-8.09,37.12-22.61,45.17-45.83,3.07-8.85,4.36-25.17,12.91-31.09,13.68-9.47,24.05,3.61,28.11,15.65,5.67,16.79,7.2,30.82,20.37,44.63,16.19,16.99,35.71,15.07,53.62,24.38,3.37,1.75,6.17,6.34,10.28,7.22ZM776.74,289.16c-4.56.64-5.93,9.79-7.87,13.23-4.92,8.72-16.76,21.22-24.93,27.07-3.39,2.43-27.4,12.25-17.91,17.04,1.57.79,3.84.48,5.69,1.3,14.82,6.58,27.91,19.11,36.1,32.9,2.41,4.06,4.1,13.24,9.67,13.25,5.06.01,6.38-9.04,8.68-13.25,7.33-13.41,22.11-26.66,36.1-32.9,2.84-1.26,8.25-.22,7.76-5.23-.43-4.4-10.52-7.04-13.94-9.15-10.29-6.37-25.31-20.31-30.97-31.03-1.68-3.19-3.2-13.95-8.4-13.22Z"/>
<path d="M372.75.26c17.02-2.7,21.7,16.54,25.74,28.78.66,2,.35,4.18.97,6.03,13.37,39.65,24.62,56.65,65.57,70.43,12.7,4.27,34.46,6.35,40.83,19.17,3.04,6.11.84,15.15-3.82,19.9-7.28,7.4-39.68,13.7-51.41,18.59-18.8,7.84-36.26,25.05-44.04,43.96-4.84,11.74-11.29,45.73-19.62,52.38-9.65,7.7-21.8,4.05-27.82-6.11-7.31-12.34-9.82-36.44-16.95-51.05-15.65-32.09-39.88-37.6-70.19-47.81-6.54-2.2-20.02-5.97-23.84-11.16-7.42-10.08-4.71-18.67,3.87-26.8,29-11.22,57.77-13.95,78.99-39.01,16.65-19.67,17.52-40.92,26.48-63.52,2.43-6.14,8.52-12.7,15.25-13.77ZM373.63,45.09c-3.49,2.24-2.14,6.63-3.08,9.98-8.8,31.51-36.03,59.55-66.76,70.24-3.39,1.18-14.11,1.7-14.73,5.33-.78,4.51,19.66,9.4,23.63,11.19,23.74,10.71,47.33,35.36,55.8,60.2,1.49,4.36,1.94,11.23,3.41,14.59.55,1.24.82,2.54,2.58,2.47,1.5-.08,2.17-1.03,2.94-2.13,3.14-4.52,5.53-17.08,8.16-22.84,9.96-21.83,30.88-42.46,52.72-52.28,5.99-2.69,14.22-4.36,20.1-6.9,13.11-5.67-3.32-7.16-6.66-8.15-28.02-8.37-54.24-31.06-66.23-57.77-1.97-4.39-6.53-21.22-8.95-23.05-.95-.72-1.67-1.23-2.94-.86Z"/>
<path d="M636.71,457.23c20.94-4.69,20.95,15.78,25.59,28.99,5.11,14.55,14.97,26.74,29.53,32.47,9.09,3.58,25.95,5.73,31.6,13.4,9.34,12.66-1.53,22.37-13.25,26.61-18.27,6.61-33.98,7.98-43.9,28.1-6.28,12.74-8.15,46.13-30.23,38.68-10.99-3.71-14.09-28.07-18.87-38.13-5.97-12.57-17.69-20.98-30.64-25.36-3.51-1.19-7.43-1.16-10.72-2.28-6.67-2.26-17.88-7.24-18.84-15.15-2.53-20.97,22.31-20.38,35.96-26.09,8.39-3.51,21.6-16.31,25.26-24.74,4.28-9.87,6.48-33.79,18.51-36.49ZM676.76,545.79c5.98-5.71-8.51-11.82-11.7-14.33-6.14-4.82-12.27-10.92-16.47-17.53-1.39-2.19-3.02-9.96-6.08-9.97-2.5,0-11.59,15.35-14.51,18.56-5.69,6.22-13.07,9.73-19.02,14.98-8.94,7.88,1.05,8.3,5.39,10.67,4.81,2.63,14.75,12.4,18.08,16.92,2.63,3.58,6.01,13.02,10.06,13.03s3.7-4.8,5.7-8.4c2.93-5.27,16.66-19.78,21.87-22.13,1.99-.9,5.35-.52,6.69-1.81Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.5 16C30.5 16.5682 30.1791 17.0877 29.6709 17.3418L3.6709 30.3418C3.12081 30.6168 2.45842 30.5281 2 30.1182C1.54166 29.7082 1.37965 29.0596 1.5918 28.4824L6.18457 16L1.5918 3.51758C1.37965 2.94039 1.54166 2.29185 2 1.88183C2.45842 1.47186 3.12081 1.38316 3.6709 1.6582L29.6709 14.6582L29.8525 14.7656C30.2544 15.043 30.5 15.5029 30.5 16Z" stroke="#6D6868" stroke-width="3" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

View File

@@ -0,0 +1,9 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="25.338" height="25.338" fill="url(#pattern0_27_28523)"/>
<defs>
<pattern id="pattern0_27_28523" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_27_28523" transform="scale(0.0078125)"/>
</pattern>
<image id="image0_27_28523" width="128" height="128" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACxQAAAsUBidZ/7wAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAq5SURBVHic7Z17sJdFGcc/PzjG4aKeIxCVIY4CR9SkAkVGQsWcGq+jlJdK05pm7I/SssuYTCn+UWOXyVS0sbEmYqzGSisHUAyUSqCRwhsdTTIvJy4RIBcFOuf0x3N+44+f57fPvvvuLkd+z2fmmWFY2Gd/77Pv++67++x3Kxi5GQmcBswEjgUmAIcA7cAuYAfwItAJrAIeBp7eHw014jEIuAD4HbAX6C1ofweuB0bnbrhRnouAtRQPen+2E/gu0Jb1FxhBjAMeJE7g6209cHG+n2IU5Tzgv6QJfq3dBbRm+k2GJ58Fukkf/KotAw7N8cMMnavJF/haWwmMCGlwJeQ/Gf1yCbAAGfH70g2sQ97pG4FhwLuBIyh+Vy8Gzu6r08jMJOT73feOXQRcSePPuhbgdOAWYGuBem+K/ssMlRbgr/gF6DHgAwXrHwV8H9jtUX83cEqpX2MU5hr8gn8zxV4P9cwANnj4WYN0SiMDBwObcQekB/h0JH/jgGcVf73AFZH8GQpfRg/GNyL7PAbYovjspNzTxvCgAjyHOxC/Jc2X1rmK317gjAR+jRqm4w7AHmSlLxWLFf8/TujbAObiDsC8xP7fi4wvGvn/NzbPk5RHcXeAkzK0YbXShmO1CmygEM57HGWvAH/J0Ib7lXJXGwHrAKG8A/ea/DLkDkzNUqW8Q6vAOkAYWmbOy1laoftRM4isA4Shrbytz9IK6ML9pDlEq8A6QBoGZ/TjGun3aBVYBwhjh1L+ziyt0P1s1yqwDhBGl1I+NksrJG/Ahfoqsg4QxmYk568Rs8jzGjhTKe/M0IamRZuKnZmhDVq6+dEZ2tC0XI/74v8ssf+Ziv8XEvtveo7DHYBu4H2JfFeAFYr/2xP5NvoYg74uvxw4KIHvTyl+e4GTE/g1kIHzVejBT7UqOA14TfH5eGSfRh+TkcROn8DX2teJszQ7mTdm/1x2YQRfRg3DgBvwy8xtZD/vqyeUc4BtHn6WY3kAUTkf2a8fGvhaWwdcSrEATQTuxZ38UbXdeCwBG34cAdxHnMDX25PAHBoHqx34BBL4PQXq/Xycn97ctADXUmynTxnbhSSXLgeeQLaIhdQzH3v0l2Y68DfCgvg15K4NUQApawtJ87nZNLQDdxK2rXshcFRNXbORDpEr+L8AhsS8GM3Gx5FVs6IXvguRf+mPKcQbODay/yGfl7agF8gEYAlhF/5W9O3bbcCP8Bu9F7VnyLPYdEAyBNmupc2m9WePA1ML+puOSL3FCHwX8AXgbUV/tCHMQtbIi174VxH1jzJr/FORcYa2mbTeupEOdAUJ9ICa5bNhDPAdZJRelF8hwX8lUlsGI52hKhTZgbxOhiIB3wK8hOwCXomkmLuSTwwHFeByYBPF7/qXaYI59dRPgA5E6mRK358PR3r7ICRhcRuigvk08AjwZ2TmKwYnII/c6QX/315EjPEm5HPOKMho4DpkpFr0rtsO3E3xoNUyHPg2YRMyy4HjS/huag5DtGx2EmfEu5TiejfnIalQRX1tRlQ8mmVMFJ3LCJ/DdlkP8i2tfXOPBX4TWP9PMBHmYIYDPyV+4Ovtn8h++HpagC8ir46ida5FZNuNQEaiJybGtJ1IUkSVafjLs9XaLmQp1iZTStCOLF3mCn7VdiPf8vMIW7hZjOXKl6YV+BNhd14noqyxkHJr30WtC5FyNSJwO/4Xfkvfvz+TxuvVExC5tZDHuWbdwG2YmnY0zsHvwr8OfA8ZJ/hSQQ5B+IenD81Wk0enp2kYiiQ1+jxuy1z4VmRbVWjgdwJfxeRSo+OjivkkMt1blgrwTQ9/9fZrRG7diEwrojvnuvgbEC3bmMxXfFbtBfb9RDQicwn6YKuoDLoPreiDw+eRCSkjIQ/gDsKChL5PV3x3k0+SpSlpxZ0+tYd9s2JTsNDhvxf4TGL/TY12By7K0IbZShvmZ2jDAYlPSvFkpVyTK43BYmRuoRFTMrThgMSnA0xUyh+O0RCFHUh+XCOOIp823wGFTwcYo5TnkkV9yVE2BDtPNwifDuCSRd1Kvrw5TZvv4CytOMAou60oZwqVpWslwKcDuGRRDyXfJMy7lHJVFtV4Mz4dYINSHmPu3weX/OpuZOnZKIhPB9DkRj8YoyEKI3CvMD6PhzK28WZ8OsAapfz8GA1R+DDufXGrM7ShaWnFLXiQYyp4kcO/TQVn4Pe4A3BPQt9nKL5tMSgDF+MOQg9wagK/Q5FXkC0H72da0RUqNwJHRvRZQZaZLSFkgHAteiCeIk5KVgX4loe/erOUsIQMRR63WhC6kF07oQxHBBKLBr9qlhSakLPwC8Ju4BZgVIG6K8DHkH2AocGvNUsLT8QP8A/CNuAO4EM03ovXgdyx2mAvxGxjSAKGoB+c3J+9hkii1m4NC5FuCTHbGhaZNtLcsZq9jgg72ubQAcBhhG0SDbXtyBikim0PHwAMQzR9Ugd/HSL6VI8JRAwQLiVMb1ezHkTpSzsE2SRiBgBtiDpXyN3Yny2h+MlXJhI1ABgFfAXZKFo0EFuBuyg/kWQycZ6k7vFHI/q8U5H08rFI8uZByBzBFkQS9SlEEnUFosYdAxOKNEpLxc7O3+S8NMs7byCJRbcAJ/KGWPREZPzUigxKtyF7IDqBVciT8T+RfDc9+1MufhoyvvE9cbT2S2UZclzs0BL+jT5yHxgxA5G9jfFltB74EnZGUBRSHxnTjswxxAh8vXVik1jRSHFo1EnIezxF8KvWDczFNsVGIeaxcR8l7PUSavdir4RolD048jL2z8GRi7DFrWjkPjp2JzIZ9iiSIxG6prKA5vmsz0LKw6PXIE+M4xr4bkPS435JscOjr4nz041aYh4f/xwycCxyp45Hjof1PT6+v2VzoyTDgBuQCxwa/HsoN5FzFjJLqPn5I/YqSMZk4DGKB39OJP8nIFPSmr+PRPJn9MMg4Cr8p3Rvi+z/RPTTyVdjT4HkjEHvBI+QZoPKJxW/vZQ7es/w4HjcAehG11gMpYIcsunyPy+Rb6OPObgDkFq9dIbi/1+J/Tc9D+IOQAr19Hq001nHaxWUlYlrZlzytJuQR3Rq7lPK+ztrcR+sA4QxEtkY04g/IGOA1CxRyju0CqwDhKFpFuaSz31RKddkfq0DBOKSzwVd1jYWpeVzrQOkIZdmYXV9oBFqfK0DhOGSz4V8qmWH457xe1WrwDpAGBuV8lw6RZpM7yatAusAYWxAtrE14jTyzMXPUso1mV+jBJpKSpn9jb5o+giTMrShabkR98W/M7H/9yv+u7AVwaScjDsAe/GYiCnBQ4r/uxP6NpC761ncQXiANHfhBYrfXuS4PyMxPuqpcyP7nISeGrYWG+BnYQSyc9cVjB7iydkfiSSWap3u8kj+DA+uRg9IL/BDymUHnYLfnoEnSvoxCjIY2TXs0wlWUXxT52hkM6rP/oBuLBVsv3AMxcSxHkJEqd7eoL4W5KCMW/FLBa/ajUUbbt+J8bgIyf0vMvjqQVTNupDZxeHINPI4ih+EuRA4lzx5CEYDPof/3RrTVmCnpgwYriTvDuGlmBr6gONs9M/DGHYHpg0wYBmLvJdTBL4L2/71luFCRAgzRuC3Azdjj/y3HIOQbeb3U2y/f9WeAa6j2BE8XthnYH7akTMWT0WEIsf3/V0bsulzO5Lt2wmsRFLM16ZqzP8B52laKFMhCbMAAAAASUVORK5CYII="/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
src/assets/images/mini.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

@@ -25,13 +25,13 @@
})
.then(() => {
MyEvent.emit('clear-generate-state')
MyEvent.emit('clearAllCache')
// MyEvent.emit('clearAllCache')
nav.path && router.push(nav.path)
})
.catch(() => {})
}
const navs = [
{ label: 'Home', icon: 'home', size: 73, path: '/workshop/home' },
{ label: 'Home', icon: 'home', size: 73, path: '/workshop/home', on: onHome },
{ label: 'Library', icon: 'library', size: 53, path: '/workshop/library' },
// { label: 'Profile', icon: 'profile', size: 55, path: '/workshop/profile' }
]

View File

@@ -1,13 +1,22 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import MyEvent from '@/utils/myEvent'
const router = useRouter()
defineProps({
title: { type: String, default: 'AI STYLING ASSISTANT' }
title: { type: String, default: 'STYLING ASSISTANT' }
})
const emit = defineEmits(['clickReturn', 'clickProfile'])
const profileVisible = ref(false)
const handleProfileVisibleChange = (visible) => {
profileVisible.value = visible
}
MyEvent.add('change-profile-visible', handleProfileVisibleChange)
const handleClickReturn = () => {
router.back()
// emit('clickReturn')
@@ -22,7 +31,9 @@
<div class="header-title">
<div class="return" @click="handleClickReturn"><SvgIcon name="return" size="34" /></div>
<span class="title">{{ title }}</span>
<div class="profile" @click="handleClickProfile"><SvgIcon name="profile" size="45" /></div>
<div class="profile" @click="handleClickProfile">
<SvgIcon :name="profileVisible ? 'profileFilledBlack' : 'profile_white'" size="45" />
</div>
</div>
</template>
@@ -49,6 +60,7 @@
color: #2c2c2c;
font-family: 'satoshiRegular';
font-size: 4rem;
letter-spacing: 0.3rem;
}
> .profile {
position: absolute;

View File

@@ -3,6 +3,7 @@
const props = defineProps({
loading: { default: false, type: Boolean },
finish: { default: false, type: Boolean },
empty: { default: false, type: Boolean },
pel: { default: () => {}, type: Function }
})
const emit = defineEmits(['load'])
@@ -23,12 +24,13 @@
})
</script>
<template>
<div class="my-list" ref="el">
<div class="my-list" ref="el" :class="{ empty: !loading && empty }">
<slot></slot>
<div class="footer">
<p v-show="!loading" class="placeholder" ref="placeholder"></p>
<span class="loading" v-show="loading">Loading...</span>
<span class="nomore" v-show="finish">No more</span>
<span class="loading" v-if="loading">Loading...</span>
<span class="empty" v-else-if="empty">Nothing Here</span>
<span class="nomore" v-else-if="finish">No more</span>
</div>
</div>
</template>
@@ -40,12 +42,28 @@
> .footer {
width: 100%;
font-size: 3rem;
color: #000;
color: #a1a1a1;
text-align: center;
margin: var(--my-list-footer-margin, 0);
> .placeholder {
height: 1px;
}
> .empty {
font-size: 4rem;
}
}
&.empty {
> .footer {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
padding: 0;
margin: 0;
> .empty {
margin-bottom: 5rem;
}
}
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="routeCache" :view-type="viewType">
<router-view v-slot="{ Component, route }" @view-type="changeViewType">
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.name" />
</keep-alive>
@@ -14,6 +14,13 @@ import { useRoute } from 'vue-router'
import { useRouter } from 'vue-router'
import MyEvent from '@/utils/myEvent'
const props = defineProps({
viewType: {
type: String,
default: ''
}
})
const route = useRoute()
// 缓存的组件名称列表
@@ -59,17 +66,6 @@ onMounted(() => {
MyEvent.add('clearAllCache', clearCache)
})
//根据viewType设置布局风格样式
const viewType = ref(0)
const changeViewType = (v: number) => {
viewType.value = v
}
const router = useRouter()
watch(
() => router.currentRoute.value,
() => (viewType.value = 0)
)
// 暴露方法供外部使用
defineExpose({
clearCache,

View File

@@ -0,0 +1,78 @@
<template>
<div class="loading-container" :style="containerStyle">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { defineProps } from 'vue'
const props = defineProps({
color: {
type: String,
default: '#000' // 默认颜色
},
size: {
type: Number,
default: 10 // 默认圆点大小px
},
spacing: {
type: Number,
default: 3 // 默认间距px调整为合适小尺寸
},
amplitude: {
type: Number,
default: 8 // 默认浮动幅度px调整为合适比例
},
duration: {
type: Number,
default: 1.2 // 默认动画持续时间s
}
})
const containerStyle = computed(() => ({
'--color': props.color,
'--size': `${props.size}px`,
'--spacing': `${props.spacing}px`,
'--amplitude': `${props.amplitude}px`,
'--duration': `${props.duration}s`
}))
</script>
<style scoped>
.loading-container {
display: flex;
justify-content: center;
align-items: center;
/* 可以根据需要调整容器高度或移除 */
}
.dot {
width: var(--size);
height: var(--size);
background-color: var(--color);
border-radius: 50%;
margin: 0 var(--spacing);
animation: wave var(--duration) infinite ease-in-out;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes wave {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(calc(-1 * var(--amplitude)));
}
}
</style>

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
//const props = defineProps({
//})
//const emit = defineEmits([
//])
let data = reactive({
})
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="gradientButton">
<div class="bg">
<slot name="content">
</slot>
</div>
</div>
</template>
<style lang="less" scoped>
.gradientButton{
position: relative;
width: 100%;
height: 100%;
--gradientButtonBorderRadius: var(--borderRadius,1rem);
--gradientButtonBorderWidth: var(--borderWidth,2px);
> .bg{
position: absolute;
inset: var(--gradientButtonBorderWidth);
display: flex;
background-color: #fff;
align-items: center;
justify-content: center;
border-radius: var(--gradientButtonBorderRadius);
overflow: hidden;
}
&::before{
content: '';
position: absolute;
width: calc(100% + 0.2rem);
height: calc(100% + 0.2rem);
top: 50%;
left: 50%;
border-radius: var(--gradientButtonBorderRadius);
transform: translate(-50%, -50%);
background: linear-gradient(156deg,
#d3d3d3 0%,
#8a8682 40%,
#8a8682 65%,
#ebebeb 100%);
}
}
</style>

View File

@@ -82,22 +82,11 @@ const {} = toRefs(data);
</div>
<div class="mask" v-if="item.id == select?.oldId"></div>
</div>
<div class="btn">
<!-- <div>
<SvgIcon v-if="!item.isLike" @click.stop="setLike(item,'like')" name="noLike" size="30" />
<SvgIcon v-else name="like" @click.stop="setLike(item,'noLike')" color="#FF4949" size="30" />
</div> -->
<!-- <div class="btn">
<div>
<SvgIcon @click.stop="updateStyle(item,index)" name="update" size="30" />
</div>
<!-- <div>
<SvgIcon v-if="!item.isAdd" @click.stop="addLibrary(item,'add')" name="add" size="30" />
<SvgIcon v-else @click.stop="addLibrary(item,'delete')" name="confirmation" size="30" />
</div> -->
<!-- <div>
<SvgIcon @click.stop="deleteStyle(index)" name="delete" size="30" />
</div> -->
</div>
</div> -->
</div>
</div>
@@ -109,7 +98,7 @@ const {} = toRefs(data);
flex-wrap: wrap;
justify-content: space-between;
> .item{
width: calc(50% - 3.1rem / 2);
width: calc(50% - 3.5rem / 2);
position: relative;
// margin-bottom: 3.3rem;
display: flex;
@@ -126,7 +115,7 @@ const {} = toRefs(data);
align-items: center;
flex-wrap: wrap;
height: 45rem;
margin: 2.4rem 0;
margin: 2.3rem 0;
background-color: #fff;
justify-content: center;
border: .6px solid #acacac;
@@ -170,26 +159,26 @@ const {} = toRefs(data);
// max-height: 50%;
}
}
> .btn{
display: flex;
align-items: center;
justify-content: flex-end;
> div{
color: #000;
margin-right: 1.2rem;
border-radius: 50%;
width: 5.2rem;
height: 5.2rem;
padding: 1rem;
background-color: #fff;
&:last-child{
margin-right: 0rem;
}
&:hover{
color: #000;
}
}
}
// > .btn{
// display: flex;
// align-items: center;
// justify-content: flex-end;
// > div{
// color: #000;
// margin-right: 1.2rem;
// border-radius: 50%;
// width: 5.2rem;
// height: 5.2rem;
// padding: 1rem;
// background-color: #fff;
// &:last-child{
// margin-right: 0rem;
// }
// &:hover{
// color: #000;
// }
// }
// }
}
}
</style>

112
src/hooks/useStreamChat.ts Normal file
View File

@@ -0,0 +1,112 @@
import { ref } from 'vue'
import { showToast } from 'vant'
import { streamChatAddress } from '@/api/workshop'
import { useUserInfoStore } from '@/stores'
/**
* 流式对话 Hook
* @param onSuccess - 成功时的回调(流式响应时调用)
* @returns { fetchMessage, isGenerating }
*/
export function useStreamChat(onSuccess?: () => void) {
const userInfoStore = useUserInfoStore()
const isGenerating = ref(false)
const fetchMessage = (message: string, sessionId: string): Promise<void> => {
isGenerating.value = true
const params = {
message,
sessionId,
gender: userInfoStore.state.generateParams.sex
}
// 直接使用 fetch 进行流式请求
const token = userInfoStore.state.token
const baseURL = import.meta.env.MODE === 'development' ? '' : import.meta.env.VITE_APP_URL
// 构建查询参数
const queryParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
queryParams.append(key, String(value))
})
const url = `${baseURL}${streamChatAddress}?${queryParams.toString()}`
return fetch(url, {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': 'application/json'
},
credentials: 'include'
})
.then(async (response) => {
// 检查响应内容类型,判断是否为流式响应
const contentType = response.headers.get('content-type') || ''
const isStreamResponse =
contentType.includes('text/event-stream') || contentType.includes('stream')
if (!response.ok) {
// 非流式错误响应,使用 text() 读取错误信息
const errorText = await response.text()
console.error('请求错误:', errorText)
showToast({
message: `failed to fetch: ${response.status}`,
position: 'top',
icon: 'none'
})
throw new Error(`发起对话错误--- ${response.status}: ${errorText}`)
}
// 不是流式响应,使用 text()读取错误信息
if (!isStreamResponse) {
const text = await response.text()
try {
const errorData = JSON.parse(text)
if (errorData.message || errorData.error) {
showToast({
message: errorData.message || errorData.error || 'network error',
position: 'top',
icon: 'none'
})
}
} catch (e) {
// 如果不是 JSON直接显示文本内容
showToast({
message: text || 'network error',
position: 'top',
icon: 'none'
})
throw new Error(text || 'network error')
}
return
}
// 流式响应处理
const reader = response.body?.getReader()
if (!reader) throw new Error('无法获取流读取器')
const decoder = new TextDecoder()
// 流式响应时调用成功回调
onSuccess?.()
})
.catch((error) => {
console.error('fetch请求失败:', error)
showToast({
message: error.message || 'network error'
})
throw error
})
.finally(() => {
isGenerating.value = false
})
}
return {
fetchMessage,
isGenerating
}
}

View File

@@ -1,16 +1,16 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useGenerateStore } from '@/stores/modules/generate'
const VerifyIDs = (num: number) => {
return true
return true
const ids = [
!!useGenerateStore().customerId,
!!useGenerateStore().visitRecordId,
!!useGenerateStore().styleId,
// !!useGenerateStore().modelPhotoId,
true,
!!useGenerateStore().originalTryOnId,
];
return ids.splice(0, num).every(id => id) ? true : "/stylist/customer";
!!useGenerateStore().customerId,
!!useGenerateStore().visitRecordId,
!!useGenerateStore().styleId,
// !!useGenerateStore().modelPhotoId,
true,
!!useGenerateStore().originalTryOnId
]
return ids.splice(0, num).every((id) => id) ? true : '/stylist/customer'
}
/**
@@ -18,7 +18,7 @@ const VerifyIDs = (num: number) => {
* 1. 设置路由的meta属性为{ cache: true },表示需要缓存
* 2. App.vue中使用RouteCache组件通过路由的name来进行匹配
* 3. 路由的name默认是文件名,如果文件名与name不一致,通过defineOptions({ name: 'componentName' })来设置
*
*
* 自定义验证规则:
* meta{ verify: => boolean || string }
* 1. boolean true 跳转 false 不跳转
@@ -48,8 +48,8 @@ const router = createRouter({
component: () => import('@/views/login/LoginPage.vue')
},
{
path:'/reset',
name:'ResetPage',
path: '/reset',
name: 'ResetPage',
component: () => import('@/views/login/ResetPage.vue')
},
{
@@ -62,16 +62,16 @@ const router = createRouter({
name: 'WelcomePage',
component: () => import('@/views/login/WelcomePage.vue')
},
{
path: '/customer',
name: 'customer',
component: () => import('@/views/login/customer.vue'),
},
{
path: '/customer',
name: 'customer',
component: () => import('@/views/login/customer.vue')
},
{
path: '/asistant',
name: 'asistant',
component: () => import('../views/asistant/index.vue'),
meta: { cache: true, verify: ()=> VerifyIDs(2) }
meta: { cache: true, verify: () => VerifyIDs(2) }
},
{
path: '/workshop',
@@ -82,115 +82,114 @@ const router = createRouter({
// path: '/workshop',
// redirect: '/workshop/selectStyle'
// },
{
path: '/workshop/home',
name: 'Home',
component: () => import('@/views/Workshop/home.vue')
},
{
path: '/workshop/homeNav',
name: 'HomeNav',
component: () => import('@/views/Workshop/homeNav.vue')
},
{
path: '/workshop/stylist',
name: 'StylistPage',
redirect: '/workshop/stylist/index',
component: () => import('@/views/stylist/container.vue'),
children: [
{
path: 'index',
name: 'index',
component: () => import('@/views/stylist/index.vue'),
meta: { verify: ()=> VerifyIDs(2) }
},
{
path: 'sex',
name: 'sex',
component: () => import('@/views/stylist/sex.vue'),
meta: { verify: ()=> VerifyIDs(2) }
},
{
path: 'dressfor',
name: 'dressfor',
component: () => import('@/views/stylist/dressfor.vue'),
meta: { verify: ()=> VerifyIDs(2) }
},
]
},
{
path: '/workshop/home',
name: 'Home',
component: () => import('@/views/Workshop/home.vue')
},
{
path: '/workshop/homeNav',
name: 'HomeNav',
component: () => import('@/views/Workshop/homeNav.vue')
},
{
path: '/workshop/stylist',
name: 'StylistPage',
redirect: '/workshop/stylist/index',
component: () => import('@/views/stylist/container.vue'),
children: [
{
path: 'index',
name: 'index',
component: () => import('@/views/stylist/index.vue'),
meta: { verify: () => VerifyIDs(2) }
},
{
path: 'sex',
name: 'sex',
component: () => import('@/views/stylist/sex.vue'),
meta: { verify: () => VerifyIDs(2) }
},
{
path: 'dressfor',
name: 'dressfor',
component: () => import('@/views/stylist/dressfor.vue'),
meta: { verify: () => VerifyIDs(2) }
}
]
},
{
path: '/workshop/selectStyle',
name: 'SelectStyle',
component: () => import('../views/Workshop/selectStyle.vue'),
meta: { verify: ()=> VerifyIDs(2) }
component: () => import('../views/Workshop/selectStyle/index.vue'),
meta: { verify: () => VerifyIDs(2) }
},
{
path: '/workshop/selectModel',
name: 'SelectModel',
component: () => import('../views/Workshop/selectModel.vue'),
meta: { verify: ()=> VerifyIDs(3) }
meta: { verify: () => VerifyIDs(3) }
},
{
path: '/workshop/product',
name: 'Product',
name: 'product',
component: () => import('../views/Workshop/product.vue'),
meta: { verify: ()=> VerifyIDs(4) }
meta: { verify: () => VerifyIDs(4) }
},
{
// 推荐try on
path: '/workshop/recommended',
name: 'recommended',
component: () => import('../views/Workshop/recommended.vue'),
meta: { verify: ()=> VerifyIDs(5) }
meta: { verify: () => VerifyIDs(5) }
},
{
// 上传照片1
path: '/workshop/uploadFace',
name: 'uploadFace',
component: () => import('../views/Workshop/uploadFace1.vue'),
meta: { verify: ()=> VerifyIDs(5) }
meta: { verify: () => VerifyIDs(5) }
},
{
// 上传照片2
path: '/workshop/uploadFace2',
name: 'uploadFace2',
component: () => import('../views/Workshop/uploadFace2.vue'),
meta: { verify: ()=> VerifyIDs(5) }
meta: { verify: () => VerifyIDs(5) }
},
{
// 自定义创作
path: '/workshop/customize',
name: 'customize',
component: () => import('../views/Workshop/customize.vue'),
meta: { verify: ()=> VerifyIDs(5) }
meta: { verify: () => VerifyIDs(5) }
},
{
// library
path: '/workshop/library',
name: 'library',
component: () => import('../views/Workshop/library.vue'),
meta: { verify: ()=> VerifyIDs(2) }
meta: { verify: () => VerifyIDs(2) }
},
{
path: '/workshop/profile',
name: 'profile',
component: () => import('../views/Workshop/profile.vue'),
meta: { verify: ()=> VerifyIDs(1) }
meta: { verify: () => VerifyIDs(1) }
},
{
// creation
path: '/workshop/creation',
name: 'creation',
component: () => import('../views/Workshop/creation/index.vue'),
meta: { verify: ()=> VerifyIDs(2) }
meta: { verify: () => VerifyIDs(2) }
},
{
// 完成创建
path: '/workshop/end',
name: 'end',
component: () => import('../views/Workshop/end.vue'),
meta: { verify: ()=> VerifyIDs(2) }
meta: { verify: () => VerifyIDs(2) }
}
]
}

View File

@@ -5,6 +5,7 @@ const store = createPinia()
// 使用持久化插件(全局持久化)
store.use(createPersistedState())
export default store
export * from './modules/h_generate'
export * from './modules/generate'
export * from './modules/overall'
export * from './modules/userInfo'

View File

@@ -2,6 +2,7 @@
import { defineStore } from 'pinia'
import MyEvent from '@/utils/myEvent'
MyEvent.add('clear-generate-state', () => useGenerateStore().clearGenerateData())
MyEvent.add('clear-client-state', () => useGenerateStore().clearCustomerInfo())
export const useGenerateStore = defineStore({
id: 'generate', // 必须指明唯一的pinia仓库的id
@@ -9,9 +10,17 @@ export const useGenerateStore = defineStore({
return {
style: {
id: '',
oldId: '' //表示从生成页面返回回来需要调整的样式id
path: '',
taskId:'',
isLike: false, //是否喜欢
status: ''
},
styleList: [{}, {}, {}, {}],
styleList: [
{id:'',taskId:'',status:'',path:''},
{id:'',taskId:'',status:'',path:''},
{id:'',taskId:'',status:'',path:''},
{id:'',taskId:'',status:'',path:''},
],
model: {
id: ''
},
@@ -68,7 +77,7 @@ export const useGenerateStore = defineStore({
/** 进店记录id */
visitRecordId: (state) => state.customerInfo.visitRecordId,
/** 服装id */
styleId: (state) => state.style.id || state.style.oldId,
styleId: (state) => state.style.id,
/** 模特照片id */
modelPhotoId: (state) => state.model.id,
/** 原始试穿id不包含魔改id */
@@ -82,45 +91,39 @@ export const useGenerateStore = defineStore({
},
actions: {
selectStyle(data: any) {
this.style.id = data.id
},
//生成后去掉id 设置oldId来修改样式
useStyleGenerate() {
if (!this.style.id) return
this.style.oldId = this.style.id
// this.style.id = ''
},
updateStyle(data) {
if (data.id == this.style.oldId) {
this.style.oldId = ''
}
if(data.id == this.style.id) {
this.style.id = ''
this.style = {
...data,
}
console.log(this.style)
},
//模特相关
selectModel(data: any) {
this.model.id = data.id
},
setIsGenerate(isGenerate: boolean) {
this.isGenerate = isGenerate
},
clearProductData() {
this.styleList = [{}, {}, {}, {}]
this.styleList = [
{id:'',taskId:'',status:'',path:''},
{id:'',taskId:'',status:'',path:''},
{id:'',taskId:'',status:'',path:''},
{id:'',taskId:'',status:'',path:''},
]
this.style = {
id: '',
oldId: ''
path: '',
isLike: false,
taskId:'',
status: ''
}
this.model = {
id: ''
}
this.clearTryOn()
},
clearTryOn() {
this.originalTryOn = {
id: '',
isLike: false,
tryOnUrl: ''
}
this.isGenerate = false
},
/** 更新顾客照片信息 */
updatePhotoInfo(data: any) {
@@ -174,7 +177,7 @@ export const useGenerateStore = defineStore({
this.updatePhotoInfo({})
this.clearCustomizeInfo()
this.clearCustomizeInfoDemo()
this.clearCustomerInfo()
// this.clearCustomerInfo()
this.setSessionId('')
},
setCustomerInfo(data: any) {

View File

@@ -0,0 +1,72 @@
// 每一个存储的模块命名规则use开头store结尾
import { defineStore } from 'pinia'
import MyEvent from '@/utils/myEvent'
MyEvent.add('clear-generate-state', () => useHGenerateStore().clearGenerateData())
export const useHGenerateStore = defineStore({
id: 'h_generate', // 必须指明唯一的pinia仓库的id
state: () => {
return {
style: {
id: '',
url: ''
},
originalTryOn: {
//生成穿好衣服的回参
id: '',
isLike: false, //是否喜欢
tryOnUrl: ''
},
/** AI魔改信息 */
customizeInfo: {
inputText: '',
count: 0,
oldInputText: '',
oldTryOnId: '',
tryOnId: '',
tryOnUrl: '',
styleUrl: '',
isRegenerated: '',
isFavorite: false
},
}
},
getters: {
/** 服装id */
styleId: (state) => state.style.id,
/** 原始试穿id不包含魔改id */
originalTryOnIdNoRein: (state) => state.originalTryOn.id,
/** 原始试穿id-优先AI魔改 */
originalTryOnId: (state) => state.customizeInfo.tryOnId || state.originalTryOn.id,
},
actions: {
/** 清空 AI魔改信息 */
clearCustomizeInfo() {
this.customizeInfo.inputText = ''
this.customizeInfo.count = 0
this.customizeInfo.oldInputText = ''
this.customizeInfo.oldTryOnId = ''
this.customizeInfo.tryOnId = ''
this.customizeInfo.tryOnUrl = ''
this.customizeInfo.styleUrl = ''
this.customizeInfo.isRegenerated = ''
this.customizeInfo.isFavorite = false
},
/** 上传服装 */
uploadStyle(data: object) {
for (const key in data) {
this.style[key] = data[key]
}
},
uploadCustomizeInfo(data: object) {
for (const key in data) {
this.customizeInfo[key] = data[key]
}
},
//设置默认数据
clearGenerateData() {
this.clearCustomizeInfo()
},
}
})

View File

@@ -9,7 +9,8 @@ export const useUserInfoStore = defineStore('userInfo', () => {
token: '',
generateParams: {
stylist: '',
sex: ''
sex: '',
stylistImage: ''
}
})
@@ -37,7 +38,8 @@ export const useUserInfoStore = defineStore('userInfo', () => {
const resetGenerateParams = () => {
state.value.generateParams = {
stylist: '',
sex: ''
sex: '',
stylistImage: ''
}
}
@@ -49,6 +51,7 @@ export const useUserInfoStore = defineStore('userInfo', () => {
removeLocal('token')
resetGenerateParams()
MyEvent.emit('clear-generate-state')
MyEvent.emit('clear-client-state')
MyEvent.emit('clearAllCache')
resolve('')
})

View File

@@ -80,7 +80,7 @@ service.interceptors.response.use(
}
const res = response.data
// 处理异常的情况
console.log(res)
// console.log(res)
if (res.code != 0) {
showToast({
message: res.errMsg || res.message,

View File

@@ -156,4 +156,31 @@ export async function DownloadImages(list: Array<{ url: string, name?: string }>
*/
export function encryptPassword(password: string): string {
return CryptoJS.MD5(password).toString()
}
/**
* 图片分享到WhatsApp
* @param url 图片URL
* @returns 无
*/
export async function shareImageToWhatsapp (url: string){
// 把图片 URL 转为 Blob
const blob = await fetch(url).then((res) => res.blob())
// 创建文件对象
const file = new File([blob], 'image.jpg', { type: 'image/jpeg' })
// 判断浏览器是否支持文件分享
if (navigator.canShare && navigator.canShare({ files: [file] })) {
await navigator.share({
files: [file]
})
} else {
// 你可以附加一些自定义文本
const message = 'share image ' + url
// 构造WhatsApp链接
const whatsappLink = `https://api.whatsapp.com/send/?text=${encodeURIComponent(message)}`
window.open(whatsappLink, '_blank')
}
}

View File

@@ -7,23 +7,25 @@
import {
getGenerateHistoricals,
setTryOnEffectFavorite,
cancelTryOnEffectFavorite
cancelTryOnEffectFavorite,
cancelStyleFavorite,
setStyleFavorite
} from '@/api/workshop'
import { useRouter } from 'vue-router'
import MyEvent from '@/utils/myEvent'
const router = useRouter()
const emit = defineEmits(['view-type'])
const query = computed(() => router.currentRoute.value.query)
const visitRecordId = computed(() => query.value.visitRecordId) // 访问记录ID
import { useGenerateStore } from '@/stores'
import { useGenerateStore, useHGenerateStore } from '@/stores'
const generateStore = useGenerateStore()
const hGenerateStore = useHGenerateStore()
const props = defineProps({
// 是否单选模式
isChooseOne: { type: Boolean, default: false }
})
onMounted(() => {
emit('view-type', 1)
})
const list = reactive([])
const size = ref(10)
const page = computed(() => Math.ceil(list.length / size.value) + 1)
const loading = ref(false)
const finish = ref(false)
const selectCount = computed(() => list.filter((v) => v.selected).length)
@@ -39,19 +41,28 @@
navLst.forEach((v) => {
if (v.flowType === query.value.flowType) navActive.value = v.value
})
navLst.forEach((v) => {
if (v.flowType === query.value.active) navActive.value = v.value
})
const clickNav = (v) => {
if (v.value === navActive.value || loading.value) return
navActive.value = v.value
onLoad()
onBackChooseSave()
onLoad('reload')
}
const onLoad = () => {
const onLoad = (type?: 'reload') => {
if (type === 'reload') {
finish.value = false
list.splice(0, list.length)
}
loading.value = true
finish.value = false
list.splice(0, list.length)
const params = {
customerId: generateStore.customerId,
type: navActive.value,
isLibrary: false
isLibrary: false,
pageNum: page.value,
pageSize: size.value
}
if (props.isChooseOne) {
params['visitRecordId'] = ''
@@ -62,16 +73,16 @@
params['visitRecordId'] = generateStore.visitRecordId
}
getGenerateHistoricals(params)
.then((data) => {
data?.forEach((v) => {
.then((data: any) => {
data.records?.forEach((v) => {
const obj = {
// tryOnId: v.tryOnId,
tryOnUrl: v.tryOnUrl,
styleUrl: v.styleUrl,
isFavorite: !!v.isFavorite,
isRegenerated: !!v.isRegenerated,
url: v.url,
id: v.id,
url: v.url,
selected: false,
loading: false,
@@ -80,7 +91,7 @@
list.push(obj)
})
loading.value = false
finish.value = true
finish.value = !data.hasNext
})
.catch((err) => {
console.error(err)
@@ -97,17 +108,22 @@
}
// 详情页
const onDetailsItem = (v) => {
if (v.isRegenerated) return
if (v.isRegenerated || !v.styleUrl) return
router.push({ query: { ...query.value, styleUrl: v.styleUrl } })
}
// 喜欢
const isLoveLoading = ref(false)
const onLoveItem = (v) => {
if (isLoveLoading.value) return
const http = v.isFavorite ? cancelTryOnEffectFavorite : setTryOnEffectFavorite
var http
if (navActive.value === 'Outfit') {
http = v.isFavorite ? cancelStyleFavorite : setStyleFavorite
} else {
http = v.isFavorite ? cancelTryOnEffectFavorite : setTryOnEffectFavorite
}
isLoveLoading.value = true
v.isFavorite = !v.isFavorite
http(v.tryOnId)
http(v.id)
.then(() => {
isLoveLoading.value = false
})
@@ -145,7 +161,10 @@
alert(`现在${isShare.value ? '可以' : '不可以'}分享`)
}
const onShareItem = (v) => {
const url = v.tryOnUrl || v.url
if (url) shareImageToWhatsapp(url)
}
const onDownloadItem = async (v) => {
if (isShare.value) {
await shareImageToWhatsapp(v.tryOnUrl)
@@ -220,20 +239,18 @@
const nav = navLst.find((v) => v.value === navActive.value)
if (!selectedItem || !nav) return showToast({ message: 'Please select one only.' })
if (nav.flowType !== FlowType.H_OUTFIT) {
generateStore.clearCustomizeInfoDemo()
generateStore.uploadCustomizeInfoDemo({
tryOnId: selectedItem.id,
tryOnUrl: selectedItem.tryOnUrl,
styleUrl: selectedItem.styleUrl,
isFavorite: selectedItem.isFavorite,
isRegenerated: selectedItem.isRegenerated
})
hGenerateStore.originalTryOn.id = selectedItem.id
hGenerateStore.originalTryOn.tryOnUrl = selectedItem.tryOnUrl
hGenerateStore.originalTryOn.isLike = selectedItem.isFavorite
hGenerateStore.style.id = ''
hGenerateStore.style.url = selectedItem.styleUrl
} else {
// style
// selectedItem.id
// selectedItem.url
hGenerateStore.style.id = selectedItem.id
hGenerateStore.style.url = selectedItem.url
// selectedItem.isFavorite
}
MyEvent.emit('clear-generate-state')
router.push({ name: 'HomeNav', query: { flowType: nav.flowType } })
} else {
router.push({ name: 'end' })
@@ -257,8 +274,20 @@
</div>
</div>
<div class="list">
<my-list v-model:loading="loading" v-model:finish="finish" @load="onLoad">
<div class="item" v-for="(v, i) in list" :key="i" @click="onItem(v)" :type="navActive">
<my-list
v-model:loading="loading"
v-model:finish="finish"
@load="onLoad"
:empty="list.length === 0"
>
<div
class="item"
v-for="(v, i) in list"
:key="i"
@click="onItem(v)"
:type="navActive"
:class="{ active: (isChooseSave || isChooseOne) && v.selected }"
>
<img v-lazy="v.tryOnUrl || v.url" />
<!-- <div class="corner">
<div class="ai" v-if="v.isRegenerated">Gen-AI</div>
@@ -268,9 +297,12 @@
<div @click.stop="onLoveItem(v)">
<SvgIcon :name="`love_${v.isFavorite ? '1' : '0'}`" size="27" />
</div>
<div @click.stop="onDownloadItem(v)">
<!-- <div @click.stop="onDownloadItem(v)">
<SvgIcon name="download" size="27" v-show="!v.loading" />
<van-loading color="#000" size="3rem" v-show="v.loading" />
</div> -->
<div @click.stop="onShareItem(v)">
<SvgIcon name="share" size="27" />
</div>
</div>
<div class="icon-selected" v-show="(isChooseSave || isChooseOne) && v.selected">
@@ -356,12 +388,14 @@
.item {
width: 47%;
height: 62.2rem;
// overflow: hidden;
border-radius: var(--border-radius);
background-color: #fff;
margin-bottom: 4rem;
border: 0.1rem solid #000;
// overflow: hidden;
position: relative;
&.active {
border: 0.3rem solid #d9d9d9;
}
&[type='Outfit'] {
height: 50rem;
> img {
@@ -452,7 +486,7 @@
}
}
> .btns {
margin: 9rem 0;
margin: 5.4rem 0 6.4rem 0;
display: flex;
justify-content: center;

View File

@@ -7,16 +7,8 @@
const router = useRouter()
const route = useRoute()
const styleUrl = computed(() => router.currentRoute.value.query.styleUrl)
const emit = defineEmits(['view-type'])
watch(
() => router.currentRoute.value,
() => emit('view-type', 1)
)
const isChooseOne = computed(() => route.query.flowType === FlowType.HISTORY)
onMounted(() => {
emit('view-type', 1)
})
</script>
<template>

View File

@@ -1,23 +1,23 @@
<script setup lang="ts">
import GenerateLoading from '@/views/asistant/components/GenerateLoading.vue'
import gradientButton from '@/components/gradientButton.vue'
import { ref, onMounted, computed } from 'vue'
import {
generateTryOnEffect,
generateTryOnEffectDemo,
setTryOnEffectFavorite,
cancelTryOnEffectFavorite
} from '@/api/workshop'
const emit = defineEmits(['viewType'])
import { useRouter, useRoute } from 'vue-router'
import { useGenerateStore } from '@/stores'
import { useGenerateStore, useHGenerateStore } from '@/stores'
import { FlowType, IsHistoryFlow } from '@/types/enum'
const generateStore = useGenerateStore()
const hGenerateStore = useHGenerateStore()
const router = useRouter()
const route = useRoute()
const query = computed(() => route.query)
const isHistoryFlow = computed(() => IsHistoryFlow(query.value.flowType))
const customizeInfo = isHistoryFlow.value
? generateStore.customizeInfoDemo
? hGenerateStore.customizeInfo
: generateStore.customizeInfo
const loading = ref(false)
const onSend = () => {
@@ -53,7 +53,7 @@
}
if (isHistoryFlow.value) {
data['originalTryOnId'] =
type === 'reload' ? customizeInfo.oldTryOnId : generateStore.customizeInfoDemo.tryOnId
type === 'reload' ? customizeInfo.oldTryOnId : hGenerateStore.originalTryOnId
} else {
data['styleId'] = generateStore.styleId
data['originalTryOnId'] =
@@ -62,7 +62,7 @@
generateTryOnEffect(data)
.then((res: any) => {
customizeInfo.count++
customizeInfo.tryOnId = res.tryOnId
customizeInfo.tryOnId = res.id
customizeInfo.tryOnUrl = res.tryOnUrl
customizeInfo.styleUrl = res.styleUrl
customizeInfo.isRegenerated = res.isRegenerated
@@ -102,7 +102,11 @@
}
const onFinish = () => {
// router.push({ name: 'creation', query: query.value })
router.push({ name: 'creation', query: { flowType: FlowType.H_AI } })
const query_ = {
...query.value,
active: FlowType.H_AI
}
router.push({ name: 'creation', query: query_ })
// if (isHistoryFlow.value) {
// router.push({ name: 'end' })
// } else {
@@ -111,12 +115,15 @@
}
// 选择另一个穿搭
const onChooseOutfit = () => {
generateStore.clearTryOn()
router.push({ name: 'SelectStyle' })
}
</script>
<template>
<div class="loading" v-if="loading"><generate-loading /></div>
<div class="loading" v-if="loading">
<generate-loading title="Generating Results..." />
</div>
<div class="customize" v-else>
<div class="title">Customize your Look!</div>
<p class="tip">Refine your Look</p>
@@ -132,11 +139,12 @@
</div>
<div class="card">
<img :src="customizeInfo.tryOnUrl" />
<div class="select-box">
<div class="history-icon"><SvgIcon name="history" size="42" /></div>
<!-- <div class="select-box">
<div class="icon"><SvgIcon name="history" size="35" /></div>
<div class="label">History</div>
<div class="icon"><SvgIcon name="xialajiantou" size="29" /></div>
</div>
</div> -->
<div class="icons">
<div @click="onLove">
<SvgIcon :name="`love_${customizeInfo.isFavorite ? 1 : 0}`" size="35" />
@@ -149,9 +157,9 @@
</div>
<div class="btns">
<button class="choose-outfit" v-if="!isHistoryFlow" @click="onChooseOutfit">
Choose Outfit
<span>Choose Outfit</span>
</button>
<button class="finish" @click="onFinish">Finish</button>
<button class="finish" @click="onFinish"><span>Finish</span></button>
</div>
</div>
</template>
@@ -226,7 +234,7 @@
margin-top: 5rem;
width: 73rem;
height: 109.5rem;
border-radius: 2rem;
border-radius: 1rem;
// box-shadow: 1.3rem 1.4rem 2rem 0.2rem #0000004d;
border: 0.2rem solid #d9d9d9;
overflow: hidden;
@@ -238,6 +246,10 @@
height: 100%;
object-fit: contain;
}
> .history-icon {
top: 4.2rem;
left: 4.2rem;
}
> .select-box {
top: 1.8rem;
left: 1.8rem;
@@ -258,15 +270,15 @@
color: #000;
}
}
> .icons {
bottom: 0.27rem;
bottom: 3.6rem;
right: 0;
height: 10rem;
display: flex;
align-items: center;
justify-content: center;
> div {
margin-right: 1.5rem;
margin-right: 2.4rem;
width: 6.2rem;
height: 6.2rem;
border-radius: 1rem;
@@ -280,28 +292,40 @@
}
> .btns {
margin-top: 4rem;
width: 68%;
min-width: 68%;
display: flex;
// justify-content: center;
justify-content: space-between;
> button {
padding: 0;
font-family: satoshiRegular;
font-family: satoshiMedium;
border: none;
width: 34rem;
height: 9.2rem;
border-radius: 1.3rem;
font-weight: 400;
font-size: 3.8rem;
border-radius: 1rem;
font-weight: 500;
font-size: 4rem;
&:active {
opacity: 0.7;
}
}
> :first-child.finish,
> .choose-outfit {
background-color: transparent;
color: #000;
border: 0.2rem solid #000;
background: linear-gradient(165deg, #b3b3b3 0%, #000 50%, #b3b3b3 100%);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #fff;
background-clip: padding-box;
border: 0.25rem solid transparent;
border-radius: 1rem;
}
}
> :first-child.finish {
width: 87.5rem;

View File

@@ -2,10 +2,7 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const emit = defineEmits(['view-type'])
onMounted(() => {
emit('view-type', 1)
})
const onExit = () => {
console.log('exit')
}
@@ -16,7 +13,9 @@
<div class="content">
<div class="title">Thank you.</div>
<div class="tip">
We are starting to learn your preferences, Looking forward to see you again,
We are starting to learn your preferences,
<br />
Looking forward to see you again,
</div>
</div>
</div>
@@ -36,21 +35,19 @@
background-size: cover;
> .content {
position: absolute;
top: 12.9rem;
left: 6rem;
// width: 100%;
top: 15.4rem;
left: 9.6rem;
display: flex;
flex-direction: column;
> .title {
font-family: satoshiBold;
font-size: 14.7rem;
font-size: 11rem;
line-height: 124%;
}
> .tip {
margin-top: 2.5rem;
width: 58.2rem;
font-family: satoshiRegular;
font-size: 4.1rem;
font-size: 4rem;
line-height: 132%;
}
}

View File

@@ -7,9 +7,6 @@ import { FlowType } from '@/types/enum'
//const props = defineProps({
//})
const emit = defineEmits([
'view-type'
])
// const data = reactive({
// })
@@ -22,9 +19,6 @@ const historicalReview = ()=>{
router.push(`/workshop/creation?flowType=${FlowType.HISTORY}`)
}
onMounted(()=>{
emit('view-type', 1)
})
onUnmounted(()=>{
})
defineExpose({})

View File

@@ -6,15 +6,12 @@ const route = useRoute()
import { showConfirmDialog } from 'vant'
import MyEvent from '@/utils/myEvent'
import { FlowType, IsHistoryFlow } from '@/types/enum'
import { useGenerateStore } from '@/stores'
const generateStore = useGenerateStore()
import { useHGenerateStore } from '@/stores'
const hGenerateStore = useHGenerateStore()
//const props = defineProps({
//})
const emit = defineEmits([
'view-type'
])
const navList = ref([])
const navDisabledList = ref([])
@@ -22,20 +19,6 @@ const navDisabledList = ref([])
// const data = reactive({
// })
const clickSwitchVIPID = ()=>{
showConfirmDialog({
title: 'Switch VIP ID?',
message: 'You have unsaved changes. Your progress will be lost.',
confirmButtonText: 'Yes',
cancelButtonText: 'Cancel',
})
.then(() => {
MyEvent.emit('clear-generate-state')
MyEvent.emit('clearAllCache')
router.push({ name: 'customer', query: { demo: 1 } })
})
.catch(() => {})
}
const openFlow = (item: any)=>{
item.click && item.click()
const query = route.query
@@ -50,15 +33,14 @@ const openFlow = (item: any)=>{
}
onMounted(()=>{
emit('view-type', 1)
let nav = [
{
path: 'selectStyle',
path: 'SelectStyle',
imgPath: new URL('@/assets/images/nav1.png',import.meta.url).href,
flowTypeList: [FlowType.H_TRYON,FlowType.H_AI],
},
{
path: 'recommended',
path: 'product',
imgPath: new URL('@/assets/images/nav2.png',import.meta.url).href,
flowTypeList: [FlowType.H_OUTFIT],
},
@@ -66,13 +48,22 @@ onMounted(()=>{
path: 'uploadFace',
imgPath: new URL('@/assets/images/nav3.png',import.meta.url).href,
flowTypeList: [FlowType.H_TRYON,FlowType.H_AI],
click(){
hGenerateStore.clearCustomizeInfo()
},
},
{
path: 'customize',
imgPath: new URL('@/assets/images/nav4.png',import.meta.url).href,
flowTypeList: [FlowType.H_TRYON,FlowType.H_AI],
click(){
generateStore.updatePhotoInfo({})
hGenerateStore.clearCustomizeInfo()
hGenerateStore.uploadCustomizeInfo({
tryOnId: hGenerateStore.originalTryOn.id,
tryOnUrl: hGenerateStore.originalTryOn.tryOnUrl,
isFavorite: hGenerateStore.originalTryOn.isLike,
styleUrl: hGenerateStore.style.url,
})
},
},
]

View File

@@ -19,7 +19,7 @@
<template>
<div class="workshop">
<header-title @clickProfile="handleClickProfile" />
<RouteCache />
<RouteCache view-type="1" />
<footer-navigation v-if="notShowFooter" />
</div>
<profile ref="profileRef" />

View File

@@ -4,14 +4,10 @@
import router from '@/router'
import { FormatDate } from '@/utils/tools'
import { getCustomerPhotos, deleteCustomerPhoto } from '@/api/workshop'
const emit = defineEmits(['view-type'])
import { showConfirmDialog } from 'vant'
import { useGenerateStore } from '@/stores'
const generateStore = useGenerateStore()
onMounted(() => {
emit('view-type', 1)
})
const loading = ref(false)
const finish = ref(false)
const list = reactive([])
@@ -47,7 +43,7 @@
cancelButtonText: 'Cancel'
}).catch(() => 0)
if (res === 0) return
console.log(obj,i)
console.log(obj, i)
deleteCustomerPhoto(obj.visitRecordId)
.then(() => {
list.splice(i, 1)
@@ -68,7 +64,12 @@
<div class="library">
<div class="title">Library</div>
<div class="list">
<my-list v-model:loading="loading" v-model:finish="finish" @load="onLoad">
<my-list
v-model:loading="loading"
v-model:finish="finish"
:empty="list.length === 0"
@load="onLoad"
>
<div class="item" v-for="(v, i) in list" :key="v.visitRecordId">
<div class="image">
<img v-lazy="v.defaultImageUrl" />
@@ -115,18 +116,18 @@
}
> .title {
font-family: satoshiRegular;
font-size: 9rem;
font-family: satoshiBold;
font-size: 8.6rem;
text-align: left;
line-height: 124%;
margin: 5rem;
margin: 5rem 9.1rem;
}
> .list {
flex: 1;
overflow: hidden;
margin: 0 3rem;
margin: 0 4.5rem;
> .my-list {
padding: 0 3.8rem;
padding: 0 4.5rem;
--my-list-footer-margin: 2rem 0;
> .item {
position: relative;
@@ -142,7 +143,7 @@
margin-top: 0;
}
> .image {
width: 22.9rem;
width: 19.2rem;
height: 100%;
overflow: hidden;
border-radius: 2rem;
@@ -150,7 +151,7 @@
> img {
width: 100%;
height: 100%;
object-fit: cover;
object-fit: contain;
display: block;
}
}
@@ -159,41 +160,33 @@
display: flex;
flex-direction: column;
height: 90%;
> .userID {
font-family: satoshiRegular;
font-weight: 500;
font-size: 3.8rem;
line-height: 89%;
color: #000;
}
> .datetime {
margin-top: 1.8rem;
font-family: satoshiRegular;
font-weight: 400;
margin-top: 1rem;
font-family: satoshiMedium;
font-weight: 500;
font-size: 3.2rem;
line-height: 89%;
color: #000;
}
> .lastopened {
margin-top: 1rem;
margin-top: 1.8rem;
font-family: satoshiRegular;
font-weight: 400;
font-style: Regular;
font-size: 2.6rem;
line-height: 89%;
color: #6f6f6f;
}
> button {
margin-top: auto;
font-family: satoshiRegular;
width: 12.3rem;
height: 3.8rem;
border-radius: 0.5rem;
box-sizing: content-box;
border: 0.193rem solid #000;
line-height: 100%;
border-radius: 0.4rem;
border: 0.2rem solid #000;
background: transparent;
font-family: satoshiRegular;
font-weight: 400;
font-size: 2.576rem;
font-size: 2.4rem;
color: #000;
margin-right: 5rem;
&:active {
@@ -207,8 +200,8 @@
right: 2rem;
width: 5.5rem;
height: 5.5rem;
border: 0.188rem solid #000;
border-radius: 1.88rem;
border: 0.188rem solid rgba(0, 0, 0, 0.5);
border-radius: 0.9rem;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -1,17 +1,15 @@
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, toRefs } from "vue";
import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue'
import { onMounted, onUnmounted, reactive, ref, toRefs, computed } from "vue";
import { useRouter, useRoute } from 'vue-router'
import GenerateLoading from '@/views/asistant/components/GenerateLoading.vue'
import { useGenerateStore } from '@/stores'
import { useGenerateStore, useHGenerateStore } from '@/stores'
import { generateTryOnEffect, setTryOnEffectFavorite, cancelTryOnEffectFavorite, addTryOnEffectComment } from '@/api/workshop'
import { FlowType } from '@/types/enum'
import { FlowType, IsHistoryFlow } from '@/types/enum'
import gradientButton from '@/components/gradientButton.vue'
const router = useRouter()
//const props = defineProps({
//})
const emit = defineEmits(['view-type'])
let data = reactive({
modelList:
[
@@ -19,8 +17,11 @@ let data = reactive({
],
isLoading: false,
})
const query = computed(() => router.currentRoute.value.query)
const generateStore = useGenerateStore()
const hGenerateStore = useHGenerateStore()
const isHistoryFlow = computed(() => IsHistoryFlow(query.value.flowType))
const vanDialogShow = ref(false)
const feedbackForm = ref({
@@ -34,11 +35,10 @@ const feedbackForm = ref({
const onContinue = ()=>{
const query = router.currentRoute.value.query
if(query?.flowType == FlowType.MAIN){
router.push({ path: 'uploadFace', query: {...query} })
if(!isHistoryFlow.value){
router.push({ path: 'uploadFace', query: {...query.value} })
}else{
router.push({ path: 'creation', query: {...query} })
router.push({ path: 'creation', query: {...query.value, active: FlowType.H_TRYON } })
}
}
@@ -51,7 +51,7 @@ const startGenerate = ()=>{
let value = {
customerId:generateStore.customerId,
visitRecordId:generateStore.visitRecordId,
styleId:generateStore.styleId,
styleId:isHistoryFlow.value ? hGenerateStore.styleId : generateStore.styleId,
// customerPhotoId:null,
// modelPhotoId:null,
// prompt:null,
@@ -61,10 +61,9 @@ const startGenerate = ()=>{
generateTryOnEffect(value).then((res:any)=>{
data.isLoading = false;
generateStore.originalTryOn.isLike = false
generateStore.originalTryOn.id = res.tryOnId
generateStore.originalTryOn.id = res.id
generateStore.originalTryOn.tryOnUrl = res.tryOnUrl
generateStore.useStyleGenerate()//生成后需要对选择衣服页面设置不可选中样式
generateStore.setIsGenerate(false)
// generateStore.useStyleGenerate()//生成后需要对选择衣服页面设置不可选中样式
generateStore.clearCustomizeInfo()
}).catch((error)=>{
@@ -120,8 +119,7 @@ const handleSubmit = ()=>{
}
onMounted(() => {
emit('view-type', 1)
if (generateStore.isGenerate) {
if (!generateStore.originalTryOn.id) {
startGenerate()
}
})
@@ -134,7 +132,7 @@ const { isLoading } = toRefs(data);
<div class="product" v-if="!isLoading">
<div class="text">
<div class="title">
Go with this Look?
{{ isHistoryFlow?'Generate Result':'Go with this look?' }}
</div>
</div>
<div class="selectContent">
@@ -150,14 +148,25 @@ const { isLoading } = toRefs(data);
<div class="operation">
<div @click="setLike"><SvgIcon :name="`love_${generateStore.originalTryOn.isLike ? '1' : '0'}`" size="35" /></div>
<div @click="feedback"><SvgIcon name="pen" size="40" /></div>
<div @click="startGenerate"><SvgIcon name="reload" size="35" /></div>
<!-- <div><SvgIcon name="download" size="35" /></div> -->
</div>
</div>
</div>
<div class="again">
<!-- <div @click="changeModel">Change Model</div> -->
<button class="flex flex-center" @click="onContinue" style="margin-left: auto;">Continue</button>
<div class="btn">
<div class="btnItem style1" @click.stop="startGenerate()">
<gradientButton>
<template #content>
<div class="text">
<span class="icon">
<SvgIcon name="reTry" size="40" />
</span>
Re-try
</div>
</template>
</gradientButton>
</div>
<div class="btnItem style2" @click.stop="onContinue">{{ isHistoryFlow?'Finish':'Continue' }}</div>
</div>
</div>
</div>
@@ -190,18 +199,18 @@ const { isLoading } = toRefs(data);
<style lang="less" scoped>
.product{
width: 100%;
// height: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
> .text{
text-align: center;
width: 100%;
margin-top: 4.3rem;
margin-top: 4rem;
> .title{
font-family: satoshiBold;
font-weight: 700;
font-size: 9.6rem;
font-size: 8.6rem;
line-height: 124%;
color: #000;
}
@@ -210,9 +219,10 @@ const { isLoading } = toRefs(data);
flex: 1;
overflow-y: auto;
padding: 0 14.1rem;
margin-top: 4.8rem;
margin-top: 4.3rem;
> .history{
width: 30.2rem;
margin-left: calc(7% / 2);
height: 6.52rem;
border-radius: 0.91rem;
border: 0.24rem solid #000;
@@ -229,7 +239,7 @@ const { isLoading } = toRefs(data);
}
}
> .modelBox{
margin-top: 2.5rem;
margin-top: 4.4rem;
> .model{
border: .2rem solid #D9D9D9;
// height: 110rem;
@@ -262,25 +272,40 @@ const { isLoading } = toRefs(data);
}
}
}
> .again{
margin-top: 4.4rem;
> .btn{
display: flex;
gap: 6.6rem;
justify-content: center;
> button{
border-radius: .7rem;
border: 3px solid #000;
background-color: #000;
text-align: center;
color: #fff;
margin-top: 5.2rem;
> div {
border-radius: .96rem;
width: 33.7rem;
font-size: 4.8rem;
font-family: satoshiMedium;
font-size: 3.6rem;
width: 24.6rem;
line-height: 6.7rem;
height: 6.7rem;
box-sizing: border-box;
&:last-child{
margin-right: 0;
line-height: 9.2rem;
display: flex;
justify-content: center;
&.style1{
--borderRadius: .96rem;
--borderWidth: 2px;
.text{
width: 100%;
text-align: center;
> .icon{
left: 4rem;
top: 50%;
transform: translateY(-50%);
position: absolute;
}
}
}
&.style2{
color: #fff;
background-color: #000;
}
}
.btnItem .text{
color: #000;
}
}
}
@@ -329,6 +354,7 @@ const { isLoading } = toRefs(data);
font-weight: 700;
line-height: 2rem;
margin-bottom: 1.8rem;
color: #000;
}
> .info{
font-size: 3.2rem;

View File

@@ -1,26 +1,44 @@
<script setup lang="ts">
import { ref, reactive, onMounted, inject } from 'vue'
import { ref, reactive, onMounted, inject, watch } from 'vue'
import router from '@/router'
import { showConfirmDialog } from 'vant'
import { useUserInfoStore, useOverallStore } from '@/stores'
import { showConfirmDialog, showToast } from 'vant'
import { useUserInfoStore, useOverallStore, useGenerateStore } from '@/stores'
import { LogOut } from '@/api/login'
import { getCustomerList, type CustomerListParams, customerCheckin } from '@/api/workshop'
import MyEvent from '@/utils/myEvent'
import { encryptPassword } from '@/utils/tools'
import { updateUserInfo } from '@/api/login'
const props = defineProps<{
isCustomer?: boolean
}>()
const userInfoStore = useUserInfoStore()
const overallStore = useOverallStore()
const emit = defineEmits(['view-type', 'selected-customer'])
const generateStore = useGenerateStore()
const emit = defineEmits(['selected-customer', 'change-visible'])
const show = ref(false)
const isEdit = ref(false)
const form = reactive({
name: { msg: '', value: userInfoStore.state.userInfo.username },
email: { msg: '', value: userInfoStore.state.userInfo.email },
password: { show: false, msg: '', value: userInfoStore.state.userInfo.password }
name: { msg: '', value: '' },
email: { msg: '', value: '' },
password: { show: true, msg: '', value: '' }
})
const open = () => {
form.name.value = userInfoStore.state.userInfo.username
form.email.value = userInfoStore.state.userInfo.email
form.password.value = ''
isEdit.value = false
show.value = true
}
const close = () => {
show.value = false
}
watch(show, (newVal) => {
emit('change-visible', newVal)
MyEvent.emit('change-profile-visible', newVal)
})
const onEditItem = (item) => {
if (!form[item]) return
form[item].edit = true
@@ -34,14 +52,38 @@ const switchCustomer = () => {
handleShowPopup(true)
}
const edit = () => {
form.password.value = ''
isEdit.value = true
}
const confirm = () => {
if (!isEdit.value) return
const password = form.password.value
const params = {
username: form.name.value,
email: form.email.value,
password
}
if (password) {
if (password.length < 6) {
return showToast('Password must be at least 6 characters')
} else {
params.password = encryptPassword(password)
}
} else {
params.password = userInfoStore.state.userInfo.password
}
overallStore.setLoading(true)
setTimeout(() => {
updateUserInfo(params).then((res) => {
overallStore.setLoading(false)
showToast('Update success')
userInfoStore.setUserInfo({
...userInfoStore.state.userInfo,
...params
})
form.password.value = ''
isEdit.value = false
}, 1000)
})
}
const logout = () => {
showConfirmDialog({
@@ -60,22 +102,89 @@ const logout = () => {
}
const showSwitchCustomerPopup = ref(false)
const custmerParams = ref<CustomerListParams>({
current: 1,
size: 5,
desc: true
})
const customerList = ref<any[]>([])
const pager = reactive({
loading: false,
noMore: false,
total: null as number | null
})
const customerListEl = ref<HTMLElement | null>(null)
const customerList = ref([
{ id: 'CUST001', nickname: 'Customer One', checked: true },
{ id: 'CUST002', nickname: 'Customer Two', checked: false },
{ id: 'CUST003', nickname: 'Customer Three', checked: false },
{ id: 'CUST004', nickname: 'Customer Four', checked: false },
{ id: 'CUST005', nickname: 'Customer Five', checked: false }
])
const loadCustomers = async (reset = false) => {
if (pager.loading) return
if (reset) {
custmerParams.value.current = 1
pager.noMore = false
customerList.value = []
}
if (pager.noMore) return
pager.loading = true
try {
const res: any = await getCustomerList(custmerParams.value)
let list: any[] = []
const pages = res?.pages ?? res?.data?.pages ?? null
if (res && Array.isArray(res.records)) {
list = res.records
pager.total = res.total ?? pager.total
} else if (res && Array.isArray(res.data)) {
list = res.data
} else if (res && res.data && Array.isArray(res.data.list)) {
list = res.data.list
pager.total = res.data.total ?? pager.total
}
if (reset) customerList.value = list
else customerList.value = customerList.value.concat(list)
if (pages != null) {
if (custmerParams.value.current >= pages) {
pager.noMore = true
} else {
custmerParams.value.current += 1
}
} else {
if (list.length === 0 || list.length < custmerParams.value.size) {
pager.noMore = true
} else {
custmerParams.value.current += 1
}
}
} catch (e) {
// ignore error for now
} finally {
pager.loading = false
}
}
const onScroll = (e: Event) => {
const el = e.target as HTMLElement
if (!el) return
if (pager.loading || pager.noMore) return
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) {
loadCustomers(false)
}
}
// 打开customer选择时关闭profile弹窗 如果不是点击confirem关闭则重新打开profile弹窗
const handleShowPopup = (flag: boolean) => {
let isCustomerOnly = false
const handleShowPopup = (flag: boolean, customer: boolean) => {
console.log(flag,customer)
// customer: 是否是顾客页面只展示customer选择弹窗
isCustomerOnly = customer
showSwitchCustomerPopup.value = flag
show.value = !flag
if (props.isCustomer) return
show.value = !flag
if (flag) {
loadCustomers(true)
}
}
const handleChangeChecked = (item) => {
customerList.value.forEach((customer) => {
customer.checked = customer.id === item.id
customer.checked = customer.vipId === item.vipId
})
}
const handleSelectCustomer = () => {
@@ -83,14 +192,32 @@ const handleSelectCustomer = () => {
if (selectedCustomer) {
emit('selected-customer', selectedCustomer)
}
if (!isCustomerOnly) {
// show.value = true
customerCheckin({ nickname: selectedCustomer.name }).then((res) => {
useUserInfoStore().resetGenerateParams()
MyEvent.emit('clear-generate-state')
useGenerateStore().setCustomerInfo(res)
router.push({ path: '/workshop/home' })
})
}
showSwitchCustomerPopup.value = false
}
const openSwitchCustomerPopup = (flag = true) => {
showSwitchCustomerPopup.value = flag
const handleFetchCustomerList = () => {
loadCustomers(true)
}
MyEvent.add('update-customer-list', handleFetchCustomerList)
defineExpose({ open, close, openSwitchCustomerPopup })
// const openSwitchCustomerPopup = (flag = true) => {
// showSwitchCustomerPopup.value = flag
// }
onMounted(() => {
handleFetchCustomerList()
})
defineExpose({ open, close, handleShowPopup })
</script>
<template>
@@ -100,7 +227,7 @@ defineExpose({ open, close, openSwitchCustomerPopup })
<span class="title">Profile</span>
<van-icon name="cross" class="close" @click="close" />
</div>
<div class="box">
<form class="box" @submit.prevent.stop="confirm">
<div class="form-item">
<div class="label">Your Name</div>
<label class="input">
@@ -110,6 +237,7 @@ defineExpose({ open, close, openSwitchCustomerPopup })
placeholder="Enter your name"
v-model="form.name.value"
:readonly="!isEdit"
required
/>
<!-- <div class="icon" v-if="isEdit" @click.stop="onSaveItem('name')">
<SvgIcon name="confirmation" size="37" />
@@ -140,7 +268,7 @@ defineExpose({ open, close, openSwitchCustomerPopup })
</label>
<p class="error" v-show="form.email.msg">{{ form.email.msg }}</p>
</div>
<div class="form-item">
<div class="form-item" v-if="isEdit">
<div class="label">Password</div>
<label class="input">
<div class="icon" @click="form.password.show = !form.password.show">
@@ -163,7 +291,7 @@ defineExpose({ open, close, openSwitchCustomerPopup })
</div>
<template v-if="isEdit">
<button class="confirm" @click="confirm">Confirm</button>
<button type="submit" class="confirm">Confirm</button>
<p class="tip">Powered by AiDLab for Lane Crawford</p>
</template>
<template v-else>
@@ -171,7 +299,7 @@ defineExpose({ open, close, openSwitchCustomerPopup })
<button class="edit" @click="edit">Edit Profile</button>
<button class="logout" @click="logout">Log out</button>
</template>
</div>
</form>
</div>
</van-popup>
<van-popup
@@ -183,18 +311,18 @@ defineExpose({ open, close, openSwitchCustomerPopup })
>
<div class="popup-title flex">
<div class="title-txt">Saved Customer ID</div>
<SvgIcon name="close_nocolor" color="#a1a1a1" size="40" @click="handleShowPopup(false)" />
<SvgIcon name="close_nocolor" color="#a1a1a1" size="40" @click="handleShowPopup(false,false)" />
</div>
<div class="cusomter-list">
<div ref="customerListEl" class="cusomter-list" @scroll="onScroll">
<div
class="customer-item flex flex-align-center flex-between"
v-for="item in customerList"
:key="item.id"
v-for="(item, index) in customerList"
:key="index + 'customer'"
@click="handleChangeChecked(item)"
>
<div class="info">
<div class="name">{{ item.nickname }}</div>
<div class="id">{{ item.id }}</div>
<div class="name">{{ item.name }}</div>
<div class="id">{{ item.vipId }}</div>
</div>
<div class="select-box">
<div class="check-box flex flex-center" v-show="!item.checked">
@@ -203,8 +331,13 @@ defineExpose({ open, close, openSwitchCustomerPopup })
<img v-show="item.checked" class="checked-icon" src="@/assets/images/checked.png" />
</div>
</div>
<div class="list-footer">
<div v-if="pager.loading">Loading...</div>
<div v-else-if="pager.noMore">No more</div>
</div>
</div>
<div class="confirm-btn" @click="handleSelectCustomer">Confirm</div>
<div class="van-safe-area-bottom"></div>
</van-popup>
</template>
@@ -375,6 +508,12 @@ defineExpose({ open, close, openSwitchCustomerPopup })
}
}
}
.list-footer {
text-align: center;
padding: 2rem 0;
font-size: 3rem;
color: #9b9b9b;
}
.confirm-btn {
margin: 4.9rem 12.7rem 0 13.3rem;
border: 0.25rem solid #3b3b3b;

View File

@@ -3,10 +3,7 @@
import { useRouter } from 'vue-router'
import { useGenerateStore } from '@/stores'
const generateStore = useGenerateStore()
const emit = defineEmits(['view-type'])
onMounted(() => {
emit('view-type', 1)
})
const router = useRouter()
const clickNext = () => {
generateStore.updatePhotoInfo({})

View File

@@ -6,9 +6,7 @@ import { useGenerateStore } from '@/stores'
const router = useRouter()
//const props = defineProps({
//})
const emit = defineEmits([
'view-type'
])
const generateStore = useGenerateStore()
let data = reactive({
modelList:
@@ -45,9 +43,7 @@ let data = reactive({
const setSelectedModelId = (item)=>{
generateStore.selectModel(item)
}
onMounted(()=>{
emit('view-type', 1)
})
const toProduct = ()=>{
router.push({ path: 'product' })
}

View File

@@ -1,207 +0,0 @@
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, toRefs, computed, onActivated } from "vue";
import SelectItem from "@/components/selectStyle/selectItem.vue";
import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue'
import { useRouter } from 'vue-router'
import { useGenerateStore, useUserInfoStore } from '@/stores'
import { showToast } from 'vant';
import { generateRequestOutfit, getRequestOutfit } from '@/api/workshop'
import { FlowType } from '@/types/enum'
const router = useRouter()
//const props = defineProps({
//})
const emit = defineEmits([
'view-type'
])
const generateStore = useGenerateStore()
const userInfoStore = useUserInfoStore()
let data = reactive({
select:computed(()=>generateStore.style),
styleList:computed(()=>generateStore.styleList),
})
let getGenerateTime = null as any
const selectItem = (item)=>{
if(!item.id || item.status != 'SUCCEEDED'){
return
}
generateStore.selectStyle(item)
}
const updateStyle = ({item,index})=>{
generateStore.updateStyle(item)
data.styleList[index] = {}
requestOutfit({num:1,index})
}
const toProduct = ()=>{
if(!generateStore.style.id){
showToast({ message: 'Please select a style.' });
return
}
if(generateStore.style.id != generateStore.style.oldId){
generateStore.setIsGenerate(true)
}
const query = router.currentRoute.value.query
if(query?.flowType == FlowType.MAIN){
router.push({ path: 'product', query: {...query} })
}else{
router.push({ path: 'creation', query: {...query} })
}
}
const requestOutfit = ({num,index})=>{
let value = {
"customerId": generateStore.customerId,
"checkInId": generateStore.visitRecordId,
"stylist": userInfoStore.state.generateParams.stylist,
"gender": userInfoStore.state.generateParams.sex,
"sessionId": generateStore.sessionId,
num,
}
generateRequestOutfit(value).then((rv)=>{
let rvIndex = 0
data.styleList.forEach((item,styleIndex)=>{
if(styleIndex < index)return
item.taskId = rv[rvIndex]
rvIndex++
})
getRequestOutfitList(rv)
})
}
const getRequestOutfitList = (generateList)=>{
let value = {requestIDs:generateList.join(',')}
getRequestOutfit(value).then((rv:any)=>{
let pendingList = []
rv.forEach((item)=>{
if(['RUNNING','PENDING'].includes(item.status))pendingList.push(item.requestId)
let index = data.styleList.findIndex((styleItem)=>styleItem.taskId == item.requestId)
if(index != -1){
data.styleList[index].id = item.id
data.styleList[index].path = item.path
data.styleList[index].status = item.status
}
})
if(pendingList.length > 0){
getGenerateTime = setTimeout(()=>{
getRequestOutfitList(pendingList)
},3000)
}
})
}
onMounted(()=>{
// generateStore.clearProductData()
emit('view-type', 1)
// if(!data.styleList[0]?.id)getRequestOutfitList(0)
if(getGenerateTime)clearTimeout(getGenerateTime)
if(!data.styleList[0]?.taskId){
requestOutfit({num:4,index:0})
}else if(data.styleList.filter((item)=>item?.status != 'SUCCEEDED').length > 0){
let generateList = data.styleList.map((item)=>item.taskId)
getRequestOutfitList(generateList)
}
})
onUnmounted(()=>{
if(getGenerateTime)clearTimeout(getGenerateTime)
})
defineExpose({})
const { styleList, select } = toRefs(data);
</script>
<template>
<div class="selectStyle">
<div class="text">
<div class="title">
Whats your Style?
</div>
<div class="info">
Select the outfit that matches you the most.
</div>
</div>
<div class="selectContent flex-1">
<SelectItem :selectList="styleList" v-model:select="select" @selectItem="selectItem" @updateStyle="updateStyle" />
</div>
<div class="footer">
<button class="flex flex-center" @click.stop="toProduct">Continue</button>
</div>
</div>
<!-- <div class="footer placeholder"></div> -->
</template>
<style lang="less" scoped>
.header-title {
// --header-title-background: #f6f6f6;
}
.selectStyle{
width: 100%;
flex: 1;
// height: 100%;
position: relative;
display: flex;
flex-direction: column;
background-color: #f6f6f6;
overflow: hidden;
> .text{
text-align: center;
width: 100%;
margin-top: 3rem;
margin-bottom: 4.9rem;
> .title{
font-family: satoshiBold;
font-weight: 700;
font-size: 9.6rem;
line-height: 124%;
color: #000;
}
> .info{
font-size: 4rem;
font-weight: 400;
line-height: 124%;
margin-top: 1.3rem;
color: rgba(0, 0, 0, 0.6);
}
}
.selectContent{
padding: 0 4rem;
overflow: auto;
}
}
.footer {
// position: fixed;
// margin-top: 4.4rem;
// padding: 4rem 4rem 0 0;
margin-top: auto;
margin-bottom: 5.6rem;
margin-right: 4rem;
display: flex;
align-items: center;
justify-content: flex-end;
// position: absolute;
// right: 4rem;
// bottom: 4rem;
height: 6.7rem;
// background-color: #f6f6f6;
&.placeholder{
position: relative;
}
> button {
// margin-right: 5rem;
border-radius: .7rem;
border: 3px solid #000;
background-color: #000;
text-align: center;
color: #fff;
font-family: satoshiMedium;
font-size: 3.6rem;
width: 24.6rem;
height: 6.7rem;
box-sizing: border-box;
line-height: 6.7rem;
}
}
</style>

View File

@@ -0,0 +1,391 @@
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, toRefs, computed, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useGenerateStore, useUserInfoStore, useHGenerateStore } from '@/stores'
import { showToast } from 'vant'
import { shareImageToWhatsapp } from '@/utils/tools'
import {
generateRequestOutfit,
getRequestOutfit,
setStyleFavorite,
cancelStyleFavorite,
retrieveAndRegenerate
} from '@/api/workshop'
import { FlowType, IsHistoryFlow } from '@/types/enum'
import GenerateLoading from '@/views/asistant/components/GenerateLoading.vue'
import gradientButton from '@/components/gradientButton.vue'
import StyleListDom from '@/views/Workshop/selectStyle/styleList.vue'
import { useStreamChat } from '@/hooks/useStreamChat'
const router = useRouter()
const route = useRoute()
//const props = defineProps({
//})
const generateStore = useGenerateStore()
const userInfoStore = useUserInfoStore()
const hGenerateStore = useHGenerateStore()
const query = computed(() => route.query)
const isHistoryFlow = computed(() => IsHistoryFlow(query.value.flowType))
const isLoading = ref(false)
// const loadingTitle= ref('Analyzing the Outfit...')
const loadingTitle = computed(() => {
let str = 'Analyzing the Outfit...'
if (!select.value.status) str = 'Analyzing the Outfit...'
if (select.value.status == 'RUNNING') str = 'Generating Results...'
if (select.value.status == 'PENDING' || select.value.status == 'ALMOST_DONE')
str = 'Almost there...'
return str
})
let data = reactive({
select: computed(() => generateStore.style),
styleList: computed(() => generateStore.styleList)
})
let dataDom = reactive({
styleListVue: null
})
let getGenerateTime = null as any
const updateStyle = () => {
// generateStore.updateStyle(item)
// data.styleList[index] = {}
requestOutfit({ num: 4 })
}
const setLikeStyle = (likeStyle) => {
if (!select.value.id) return
if (likeStyle) {
cancelStyleFavorite(select.value.id).then(() => {
select.value.isLike = false
})
} else {
setStyleFavorite(select.value.id).then(() => {
select.value.isLike = true
})
}
}
const setDownload = () => {
if (select.value.path) shareImageToWhatsapp(select.value.path)
}
const toProduct = () => {
// if(generateStore.style.id){
// generateStore.setIsGenerate(true)
// }
router.push({ path: 'product', query: { ...query.value } })
// if(!isHistoryFlow.value){
// router.push({ path: 'product', query: {...query.value} })
// }else{
// router.push({ path: 'creation', query: {...query.value, active: FlowType.H_OUTFIT} })
// }
}
const requestOutfit = async ({ num }) => {
isLoading.value = true
let rv: any = await new Promise<void>((resolve, reject) => {
if (isHistoryFlow.value) {
retrieveAndRegenerate({
tryOnEffectsId: hGenerateStore.originalTryOn.id,
checkInId: generateStore.visitRecordId
}).then((rv: any) => {
resolve(rv)
})
} else {
let value = {
customerId: generateStore.customerId,
checkInId: generateStore.visitRecordId,
stylist: userInfoStore.state.generateParams.stylist,
gender: userInfoStore.state.generateParams.sex,
sessionId: generateStore.sessionId,
num
}
generateRequestOutfit(value).then((rv: any) => {
resolve(rv)
})
}
})
generateStore.clearProductData()
data.select.taskId = rv[0]
rv.forEach((item, index) => (data.styleList[index].taskId = item))
getRequestOutfitList(rv)
}
const getRequestOutfitList = (generateList) => {
let value = { requestIDs: generateList.join(',') }
getRequestOutfit(value).then((rv: any) => {
let selectIndex = rv.findIndex((item) => item.requestId == data.select.taskId)
console.log(selectIndex)
if (selectIndex != -1) {
data.select.id = rv[selectIndex].id
data.select.path = rv[selectIndex].path
data.select.status = rv[selectIndex].status
}
rv.forEach((item) => {
let index = data.styleList.findIndex(
(styleListItem) => styleListItem?.taskId == item.requestId
)
data.styleList[index] = {
id: item.id,
taskId: item.requestId,
status: item.status,
path: item.path
}
})
if (['SUCCEEDED'].includes(data.select.status)) {
isLoading.value = false
if (isHistoryFlow.value) {
hGenerateStore.uploadStyle({
id: data.select.id,
path: data.select.path
})
}
}
if (data.styleList.filter((item) => item?.status == 'FAILED').length > 0) {
showToast({
message: 'One of the outfits failed to generate. Please try generating again.',
duration: 2000
})
isLoading.value = false
}
const taskIdList = data.styleList
.filter((item) => item?.taskId && item?.status !== 'SUCCEEDED' && item?.status !== 'FAILED')
.map((item) => item.taskId)
if (taskIdList.length > 0) {
getGenerateTime = setTimeout(() => {
getRequestOutfitList(taskIdList)
}, 3000)
}
})
}
const styleListInit = () => {
dataDom.styleListVue.init(data.select)
}
// 使用 useStreamChat在流式请求成功后执行原本的逻辑
const { fetchMessage, isGenerating } = useStreamChat()
onMounted(() => {
// generateStore.clearProductData()
// if(!data.styleList[0]?.id)getRequestOutfitList(0)
if (getGenerateTime) clearTimeout(getGenerateTime)
// 检查是否有从 dressfor 传递过来的消息
const message = query.value.message as string
const sessionId = query.value.sessionId as string
if (message && sessionId) {
// 有消息,说明是从 dressfor 跳转过来的,先发起流式请求
generateStore.setSessionId(sessionId)
fetchMessage(message, sessionId)
.then(() => {
console.log('对话请求完成')
// 清除 URL 参数避免返回时再次触发
router.replace({ path: '/workshop/selectStyle' })
// 开始生成outfit
requestOutfit({ num: 4 })
})
.catch(() => {
// 错误处理
})
}
// 原本的逻辑
const taskIdList = data.styleList
.filter((item) => item?.taskId && item?.status !== 'SUCCEEDED')
.map((item) => item.taskId)
if (data.select.status == 'SUCCEEDED' && taskIdList.length == 0) {
return
} else if (!data.select?.taskId) {
requestOutfit({ num: 4 })
} else if (data.select.status != 'SUCCEEDED' || taskIdList.length > 0) {
if (data.select.status != 'SUCCEEDED') isLoading.value = true
getRequestOutfitList(taskIdList)
}
})
onUnmounted(() => {
if (getGenerateTime) clearTimeout(getGenerateTime)
})
defineExpose({})
const { select } = toRefs(data)
const { styleListVue } = toRefs(dataDom)
</script>
<template>
<div class="selectStyle">
<div class="text">
<div class="title">Outfit Result</div>
<div class="info">Refine your Look</div>
</div>
<div class="selectContent">
<!-- {{ select }} -->
<div class="imgBox">
<img :src="select.path" alt="" />
</div>
<div v-if="!isHistoryFlow" class="chooseMore" @click.stop="styleListInit">
<gradientButton>
<template #content>
<div class="text">Choose More</div>
</template>
</gradientButton>
<div></div>
</div>
<div class="btn" v-else>
<div class="like" @click.stop="setLikeStyle(select.isLike)">
<SvgIcon :name="`love_${select.isLike ? 1 : 0}`" size="35" />
</div>
<div class="down" @click.stop="setDownload()">
<SvgIcon name="download" size="35" />
</div>
</div>
</div>
<div class="btn">
<div class="btnItem style1" @click.stop="updateStyle()">
<gradientButton>
<template #content>
<div class="text">
<span class="icon">
<SvgIcon name="reTry" size="40" />
</span>
Re-try
</div>
</template>
</gradientButton>
</div>
<div class="btnItem style2" @click.stop="toProduct">Continue</div>
</div>
</div>
<!-- <div class="footer placeholder"></div> -->
<div class="loading-container" v-if="isGenerating || isLoading">
<GenerateLoading :title="loadingTitle" />
</div>
<StyleListDom ref="styleListVue"></StyleListDom>
</template>
<style lang="less" scoped>
.header-title {
// --header-title-background: #f6f6f6;
}
.loading-container {
width: 100%;
height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.selectStyle {
width: 100%;
flex: 1;
// height: 100%;
position: relative;
display: flex;
flex-direction: column;
background-color: #f6f6f6;
overflow: hidden;
> .text {
text-align: center;
width: 100%;
margin-top: 8.5rem;
margin-bottom: 8.5rem;
> .title {
font-family: satoshiBold;
font-weight: 700;
font-size: 8.6rem;
line-height: 124%;
color: #000;
}
> .info {
font-size: 4rem;
font-weight: 400;
line-height: 124%;
margin-top: 3.2rem;
color: rgba(0, 0, 0, 0.6);
}
}
.selectContent {
// padding: 0 4rem;
margin: 0 auto;
width: 73.7rem;
margin-bottom: 19rem;
> .imgBox {
height: 73.7rem;
width: 100%;
margin-bottom: 5.6rem;
> img {
width: 100%;
height: 100%;
}
}
> .chooseMore {
--borderRadius: 5.4rem;
--borderWidth: 2px;
width: 24.8rem;
margin: 0 auto;
height: 7.6rem;
.text {
font-size: 3.1rem;
color: #000;
font-family: satoshiMedium;
}
}
> .btn {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 2rem;
> div {
color: #000;
border-radius: 50%;
width: 7rem;
height: 7rem;
padding: 1rem;
background-color: #fff;
&:hover {
color: #000;
}
}
}
}
> .btn {
display: flex;
gap: 6.6rem;
justify-content: center;
> div {
border-radius: 0.96rem;
width: 33.7rem;
font-size: 4.8rem;
font-family: satoshiMedium;
line-height: 9.2rem;
display: flex;
justify-content: center;
&.style1 {
--borderRadius: 0.96rem;
--borderWidth: 2px;
.text {
width: 100%;
text-align: center;
> .icon {
left: 4rem;
top: 50%;
transform: translateY(-50%);
position: absolute;
}
}
}
&.style2 {
color: #fff;
background-color: #000;
}
}
.btnItem .text {
color: #000;
}
}
}
</style>

View File

@@ -0,0 +1,121 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, toRefs } from "vue";
import SelectItem from "@/components/selectStyle/selectItem.vue";
import { useGenerateStore, } from '@/stores'
//const props = defineProps({
//})
//const emit = defineEmits([
//])
const generateStore = useGenerateStore()
let data = reactive({
showStyleList:false,
list:computed(()=>generateStore.styleList),
selectStyle:computed(()=>generateStore.style),
oldSelectStyle:{} as any,
})
const close = ()=>{
showStyleList.value = false;
}
const init = (item)=>{
data.showStyleList = true
data.oldSelectStyle = JSON.parse(JSON.stringify(item))
}
const confirm = ()=>{
if(data.selectStyle.id != data.oldSelectStyle.id){
data.selectStyle.id = data.oldSelectStyle.id
data.selectStyle.path = data.oldSelectStyle.path
data.selectStyle.status = data.oldSelectStyle.status
data.selectStyle.taskId = data.oldSelectStyle.taskId
data.selectStyle.isLike = false
}
generateStore.clearTryOn()
close();
}
const setStyle = (item)=>{
data.oldSelectStyle = item
}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({init})
const {showStyleList,list,oldSelectStyle} = toRefs(data);
</script>
<template>
<van-popup
class="user-popup"
v-model:show="showStyleList"
round
position="bottom"
teleport="body"
>
<div class="profile">
<div class="header">
<span class="title">Outfit Result</span>
<van-icon name="cross" class="close" @click="close" />
</div>
<div class="content">
<SelectItem :selectList="list" :select="oldSelectStyle" @selectItem="setStyle"></SelectItem>
</div>
<div class="bottom">
<div @click="confirm">Confirm</div>
</div>
</div>
</van-popup>
</template>
<style lang="less" scoped>
.van-popup {
max-height: 90%;
--van-popup-round-radius: 7.8rem;
}
.profile {
margin: 11.1rem 0 5.6rem;
display: flex;
flex-direction: column;
align-items: center;
> .header {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 0 8rem 0 5.2rem;
margin-bottom: 8rem;
> .title {
font-family: satoshiBold;
font-size: 4.6rem;
color: #181725;
}
> .close {
margin-left: auto;
font-size: 5rem;
color: #a1a1a1;
}
}
> .content{
width: 96.2rem;
}
> .bottom{
margin: 3rem 0 4rem;
width: 100%;
> div{
width: 24.8rem;
line-height: 6.7rem;
font-family: satoshiMedium;
font-size: 3.6rem;
text-align: center;
border-radius: .7rem;
background-color: #000;
color: #fff;
margin-right: 4.2rem;
margin-left: auto;
}
}
}
</style>

View File

@@ -4,10 +4,6 @@
import { useGenerateStore } from '@/stores'
import { IsHistoryFlow } from '@/types/enum'
const generateStore = useGenerateStore()
const emit = defineEmits(['view-type'])
onMounted(() => {
emit('view-type', 1)
})
const router = useRouter()
const route = useRoute()
const query = computed(() => route.query)
@@ -94,14 +90,7 @@
justify-content: center;
bottom: 19.7rem;
> button {
width: 35rem;
height: 8.3rem;
box-sizing: border-box;
border-radius: 0.7rem;
margin: 0 1.8rem;
&.sandblasted-blurred {
// border-width: 0.2rem;
}
margin: 0 7.5rem;
}
}
}

View File

@@ -2,14 +2,11 @@
import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { uploadCustomerPhoto } from '@/api/workshop'
import { useGenerateStore } from '@/stores'
import { useGenerateStore, useHGenerateStore } from '@/stores'
import { IsHistoryFlow } from '@/types/enum'
const generateStore = useGenerateStore()
const hGenerateStore = useHGenerateStore()
const emit = defineEmits(['view-type'])
onMounted(() => {
emit('view-type', 1)
})
const router = useRouter()
const route = useRoute()
const query = computed(() => route.query)
@@ -47,7 +44,7 @@
formData.append('file', fileData.file)
uploadCustomerPhoto(formData).then((res) => {
generateStore.updatePhotoInfo({ ...res, file: fileData.file })
isHistoryFlow.value ? generateStore.clearCustomizeInfoDemo() : generateStore.clearCustomizeInfo()
isHistoryFlow.value ? hGenerateStore.clearCustomizeInfo() : generateStore.clearCustomizeInfo()
router.push({ name: 'customize', query: query.value })
})
}
@@ -145,11 +142,7 @@
display: flex;
justify-content: center;
> button {
width: 35rem;
height: 8.3rem;
margin: 0 5rem;
// border-radius: 4.3rem;
border-width: 0.25rem;
}
}
}

View File

@@ -4,12 +4,19 @@
<img src="@/assets/images/chat_loading.png" alt="Loading" class="loading-image" />
<!-- 阴影效果 -->
<div class="loading-shadow"></div>
<div class="loading-text">{{ title }}</div>
<div class="loading-text" ref="textBox">
<span>{{ text }}</span>
<div class="loading-dot" ref="dotBox"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, watch, reactive, toRefs, nextTick, ref } from "vue";
import { gsap, TweenMax } from "gsap";
// 这个组件只负责显示loading动画
// 定义组件props类型
@@ -20,6 +27,76 @@ const props = defineProps({
}
})
const dotBox = ref(null)
const textBox = ref(null)
let tl1 = null;
const text = ref('')
watch(() => props.title, (newVal, oldVal) => {
if (newVal !== oldVal) {
destroyAnimation()
fadeOut(newVal)
}
})
function fadeOut(newVal) {
gsap.to(textBox.value,.5, {
opacity: 0,
duration: 1,
ease: "power2.in",
onComplete: () => {
// 消失后更换文字
text.value = newVal
// 然后出现
fadeIn();
}
});
}
// 出现动画
function fadeIn() {
gsap.to(textBox.value,.5, {
opacity: 1,
duration: 1,
onComplete:()=>{
setTl1();
}
});
}
const setTl1 = ()=>{
nextTick(()=>{
let el = dotBox.value
let width = el.offsetWidth + el.parentElement.offsetWidth
let time = el.parentElement.offsetWidth / el.offsetWidth / 2
console.log(time,width)
tl1 = gsap.timeline();
tl1.to(el,time,
{
ease: "power1.in",
left:width,
onComplete:()=>{
setTimeout(() => {
tl1.restart()
}, 1000)
}
},
)
tl1.progress(0);
})
}
function destroyAnimation() {
if (tl1) {
tl1.progress(0);
tl1.kill();
tl1 = null;
}
}
onMounted(() => {
text.value = props.title
setTl1()
})
</script>
@@ -36,7 +113,7 @@ const props = defineProps({
.loading-image {
width: 36.4rem;
height: 36.4rem;
animation: rotate 1s linear infinite;
animation: rotate 1.5s ease-in-out infinite;
}
.loading-shadow {
@@ -54,14 +131,29 @@ const props = defineProps({
font-size: 4.8rem;
letter-spacing: 0.02em;
line-height: 124%;
position: relative;
}
.loading-dot{
height: 100%;
aspect-ratio: 1.2/1;
background: radial-gradient(ellipse 150% 150% at center, #ffffff,rgba(255,255,255,.4), transparent);
border-radius: 50%;
position: absolute;
top: 50%;
left: 0;
transform: translate(-100%, -50%);
}
@keyframes rotate {
from {
transform: rotate(0deg);
0% {
transform: translateY(0px);
}
to {
transform: rotate(360deg);
50% {
transform: translateY(-100px);
}
100% {
transform: translateY(0px);
}
}
</style>

View File

@@ -197,7 +197,9 @@ const startRecording = () => {
// 检查浏览器支持
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
// alert('您的浏览器不支持语音识别功能')
showToast('Your browser does not support speech recognition, please try again with another browser')
showToast(
'Your browser does not support speech recognition, please try again with another browser'
)
isRecording.value = false
return
}
@@ -296,7 +298,7 @@ const stopRecording = () => {
.shortcut-item {
font-size: 4.2rem;
width: fit-content;
font-family: 'robotoRegular';
font-family: 'satoshiMedium';
white-space: nowrap;
height: 8.1rem;
line-height: 8.1rem;
@@ -351,8 +353,8 @@ const stopRecording = () => {
outline: none;
background: transparent;
font-size: 4rem;
font-family: 'robotoRegular';
font-weight: 400;
font-family: 'satoshiMedium';
font-weight: 500;
line-height: 4.8rem; /* 设置行高等于实际渲染高度,实现垂直居中 */
padding: 0;
color: #000;
@@ -361,8 +363,8 @@ const stopRecording = () => {
&::placeholder {
color: #888;
letter-spacing: -0.01em;
font-weight: 400;
font-family: 'robotoRegular';
font-weight: 500;
font-family: 'satoshiMedium';
word-spacing: -5px;
line-height: 4.8rem;
}

View File

@@ -2,14 +2,15 @@
<div class="chat-message" :class="{ 'user-message': isMyself, 'ai-message': !isMyself }">
<!-- AI消息显示头像 -->
<div v-if="!isMyself" class="chat-avatar">
<img src="@/assets/images/asistant.png" alt="AI Avatar" />
<img class="avatar" :src="thumb" alt="AI Avatar" />
</div>
<!-- 消息内容 -->
<div class="message-content">
<div class="message-text" :class="{ streaming: isStreaming }">
<div v-html="content"></div>
<span v-if="isStreaming" class="streaming-cursor">|</span>
<!-- <span v-if="isStreaming" class="streaming-cursor">|</span> -->
<WaveLoading v-if="isStreaming" />
</div>
<!-- <div v-if="!isMyself" class="message-actions flex">
<SvgIcon
@@ -27,9 +28,16 @@
<script setup lang="ts">
import { computed } from 'vue'
import MarkdownIt from 'markdown-it'
import WaveLoading from '@/components/WaveLoading.vue'
import { useUserInfoStore } from '@/stores'
const md = new MarkdownIt()
const userInfoStore = useUserInfoStore()
const thumb = computed(() => {
return userInfoStore.state.generateParams.stylistImage
})
// 定义消息类型
interface ChatMessage {
sessionId: string | number
@@ -95,13 +103,15 @@ const actionList: ActionItem[] = [
display: flex;
margin-bottom: 16px;
align-items: flex-start;
color: #000;
.message-text {
font-size: 4.2rem;
font-family: 'robotoRegular';
font-size: 4rem;
font-family: 'satoshiMedium';
line-height: 121%;
font-weight: 400;
font-weight: 500;
background-color: #efefef;
padding: 3.43rem 4.35rem;
letter-spacing: -0.01em;
}
&.user-message {
@@ -126,6 +136,10 @@ const actionList: ActionItem[] = [
color: #000;
border-radius: 0 2rem 2rem 2rem;
word-break: break-word;
:deep(strong){
font-family: 'satoshiBold';
font-size: 4.5rem;
}
}
}
}
@@ -133,16 +147,17 @@ const actionList: ActionItem[] = [
.chat-avatar {
width: 7.8rem;
height: 7.4rem;
height: 7.8rem;
border-radius: 50%;
margin-right: 1.88rem;
border: .2rem solid #000;
padding: 1.4rem;
overflow: hidden;
// border: 0.2rem solid #000;
// padding: 1.4rem;
img {
width: 4.9rem;
height: 4.9rem;
border-radius: 50%;
width: 100%;
// height: 100%;
// border-radius: 50%;
// object-fit: cover;
}
}

View File

@@ -1,34 +1,30 @@
<template>
<div class="asistant-container flex flex-column">
<div class="header">
<HeaderTitle hasSetting styleType="3" />
<HeaderTitle hasSetting styleType="3" @clickProfile="handleClickProfile" />
</div>
<div class="loading-container" v-if="isLoading">
<GenerateLoading />
<div class="content flex-1">
<NoticeList
ref="noticeListRef"
:list="messageList"
:is-streaming="isStreaming"
:streaming-message="currentStreamingMessage"
/>
</div>
<template v-else>
<div class="content flex-1" v-if="!isLoading">
<NoticeList
ref="noticeListRef"
:list="messageList"
:is-streaming="isStreaming"
:streaming-message="currentStreamingMessage"
/>
<div class="footer">
<InputArea @send-message="handleSendMessage" />
<div class="continue flex">
<div class="btn flex flex-center" @click="handleContinue">Generate</div>
</div>
<div class="footer" v-if="!isLoading">
<InputArea @send-message="handleSendMessage" />
<div class="continue flex">
<div class="btn flex flex-center" @click="handleContinue">Continue</div>
</div>
</div>
</template>
</div>
<Profile ref="profileRef" />
</div>
</template>
<script setup lang="ts">
import HeaderTitle from '@/components/HeaderTitle.vue'
import NoticeList from './components/NoticeList.vue'
import InputArea from './components/InputArea.vue'
import GenerateLoading from './components/GenerateLoading.vue'
import Profile from '../Workshop/profile.vue'
import { ref, onMounted, onUnmounted, onActivated } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserInfoStore, useGenerateStore } from '@/stores'
@@ -60,7 +56,11 @@ interface ChatMessage {
self?: boolean
}
const isLoading = ref<boolean>(false)
const profileRef = ref<InstanceType<typeof Profile> | null>(null)
const handleClickProfile = () => {
profileRef.value.open()
}
const noticeListRef = ref<NoticeListRef | null>(null)
const messageList = ref<ChatMessage[]>([])
@@ -81,6 +81,8 @@ const sendPrefilledMessage = () => {
}
onMounted(() => {
console.log('1111111111111');
sessionId.value = Math.floor(Date.now() / 1000).toString()
generateStore.setSessionId(sessionId.value)
})
@@ -273,17 +275,13 @@ const handleFetchMessage = (message: string) => {
const handleContinue = () => {
// router.push('/workshop/selectStyle')
// 模拟接口之后再跳转
isLoading.value = true
generateStore.clearProductData()
setTimeout(() => {
router.push('/workshop/selectStyle')
isLoading.value = false
}, 1000)
router.push('/workshop/selectStyle')
}
</script>
<style lang="less" scoped>
.asistant-container {
height: 100vh;
height: 100%;
overflow: hidden;
}
@@ -311,16 +309,9 @@ const handleContinue = () => {
border-radius: 0.7rem;
background-color: #000;
width: 24.6rem;
height: 5.9rem;
height: 6.7rem;
box-sizing: border-box;
}
}
}
.loading-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
}
</style>

View File

@@ -2,13 +2,16 @@
<div class="login-page">
<div class="content">
<div class="back-button" @click="goBack">
<SvgIcon name="left" size="50" color="#fff" />
<SvgIcon name="left" size="50" color="#fff" />
</div>
<div class="header">
<div class="title">Log in.</div>
<p class="subtitle">Redefine the styling experience with AI.</p>
<p class="subtitle">Use Styling Angel to speed up your fashion journey.</p>
<div class="title">Staff Login.</div>
<p class="subtitle">
<span>Experience our personalised styling journey with</span>
<br />
<span>Lane Crawford.</span>
</p>
</div>
<div class="login-container">
@@ -32,7 +35,11 @@
<img :src="google" class="google-icon" />
Sign in with Google
</div> -->
<GoogleLogin @googelLogin="handleGoogleLogin" ref="googleLoginRef" @click="clickGooleLogin"></GoogleLogin>
<GoogleLogin
@googelLogin="handleGoogleLogin"
ref="googleLoginRef"
@click="clickGooleLogin"
></GoogleLogin>
<div class="sign-up-button" @click="handleSignup">Dont have an account? Sign Up</div>
</div>
</div>
@@ -183,7 +190,7 @@ const handleSignup = () => {
cursor: pointer;
z-index: 3;
font-size: 3.4rem;
.c-svg{
.c-svg {
width: initial;
height: initial;
}
@@ -191,12 +198,14 @@ const handleSignup = () => {
.header {
margin-top: 1.42rem;
padding-left: 15.5rem;
padding-left: 14.5rem;
padding-right: 14.3rem;
color: white;
font-family: 'satoshiRegular';
.title {
font-size: 11rem;
font-weight: bold;
font-weight: 700;
margin-bottom: 0.8rem;
color: white;
font-family: 'satoshiBold';
@@ -226,11 +235,7 @@ const handleSignup = () => {
position: relative;
width: calc(100% - 28.4rem);
height: 107.8rem;
background: radial-gradient(
100% 100% at 0% 0%,
rgba(0, 0, 0, 0.4) 0%,
rgba(0, 0, 0, 0.2) 100%
);
background: radial-gradient(100% 100% at 0% 0%, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.2) 100%);
backdrop-filter: blur(35px);
-webkit-backdrop-filter: blur(35px);
-moz-backdrop-filter: blur(35px);

View File

@@ -4,14 +4,17 @@
<div class="content">
<!-- 返回按钮 -->
<div class="back-button" @click="goBack">
<img src="@/assets/images/arrow_left.png" class="back-icon" />
<SvgIcon name="left" size="50" color="#fff" />
</div>
<!-- 标题区域 -->
<div class="header">
<div class="title">Log in.</div>
<p class="subtitle">Redefine the styling experience with AI.</p>
<p class="subtitle">Use Styling Angel to speed up your fashion journey.</p>
<div class="title">Staff Login.</div>
<p class="subtitle">
<span>Experience our personalised styling journey with</span>
<br />
<span>Lane Crawford.</span>
</p>
</div>
<div class="login-container">
@@ -130,9 +133,9 @@ const handleSuccess = (data: any) => {
cursor: pointer;
z-index: 3;
font-size: 3.4rem;
.back-icon {
width: 2.83rem;
height: 3.47rem;
.c-svg {
width: initial;
height: initial;
}
}

View File

@@ -2,13 +2,12 @@
<div class="login-page">
<div class="content">
<div class="back-button" @click="goBack">
<img src="@/assets/images/arrow_left.png" class="back-icon" />
<SvgIcon name="left" size="50" color="#fff" />
</div>
<div class="header">
<div class="title">Sign up.</div>
<p class="subtitle">Redefine the styling experience with AI.</p>
<p class="subtitle">Use Styling Angel to speed up your fashion journey.</p>
<div class="title">Staff Sign up.</div>
<p class="subtitle">Start my personalised styling journey with Lane Crawford.</p>
</div>
<div class="login-container">
@@ -30,7 +29,12 @@
<button type="button" class="login-button" @click="handleConfirm">Sign Up</button>
<GoogleLogin text="Sign up with Google" @googelLogin="handleGoogleSignup" ref="googleSignupRef" @click="clickGooleLogin" />
<GoogleLogin
text="Sign up with Google"
@googelLogin="handleGoogleSignup"
ref="googleSignupRef"
@click="clickGooleLogin"
/>
</div>
</div>
</div>
@@ -183,15 +187,16 @@ const handleGoogleSignup = async (accessToken: string) => {
cursor: pointer;
z-index: 3;
font-size: 3.4rem;
.back-icon {
width: 2.83rem;
height: 3.47rem;
.c-svg {
width: initial;
height: initial;
}
}
.header {
margin-top: 1.42rem;
padding-left: 15.5rem;
padding-left: 14.5rem;
padding-right: 14.3rem;
color: white;
font-family: 'satoshiRegular';
.title {
@@ -281,7 +286,6 @@ const handleGoogleSignup = async (accessToken: string) => {
}
}
.footer {
position: relative;
text-align: center;

View File

@@ -2,10 +2,14 @@
<div class="welcome-page safe-area-top safe-area-bottom">
<div class="content">
<div class="title">
AI STYLING <br />
ASSISTANT
Styling <br />
Assistant
</div>
<p class="subtitle">Redefine the styling experience with AI.</p>
<p class="subtitle">
<span>Experience our personalised styling journey with</span>
<br>
<span> Lane Crawford.</span>
</p>
<div class="btn-container flex">
<div class="log btn" @click="goToLogin">Log in</div>
<div class="sign btn" @click="goToSignup">Sign up</div>
@@ -49,15 +53,17 @@ const goToLogin = () => {
font-weight: 400;
margin-bottom: 31.5rem;
.title {
font-family: 'boskaRegular';
line-height: 93%;
letter-spacing: -0.02em;
font-family: 'satoshiMedium';
line-height: 120%;
font-size: 11rem;
// letter-spacing: -0.02em;
}
.subtitle {
font-size: 3.2rem;
font-family: 'satoshiRegular';
margin: 3.2rem 0 6rem;
list-style: 124%;
font-weight: 400;
line-height: 124%;
letter-spacing: 0.08em;
}
.btn-container {

View File

@@ -5,7 +5,11 @@
>
<div class="setting flex flex-between">
<SvgIcon name="left" size="70" @click.stop="handleBack" />
<SvgIcon name="profile_white" size="55" @click="handleOpenProfile" />
<SvgIcon
:name="profileVisible ? 'profileFilledWhite' : 'profile_white'"
size="55"
@click="handleOpenProfile"
/>
</div>
<template v-if="pageMode === 'entry'">
<div class="content flex-1 flex flex-center flex-column">
@@ -25,8 +29,8 @@
<div class="text">
<div class="form-title">{{ formTitle }}</div>
<div class="description">
<p>Redefine the styling experience with AI.</p>
<p>Use Styling Angel to speed up your fashion journey.</p>
<p>Unlock personalized styling insights.</p>
<p>Enter a client profile to begin curating their next look with Styling Angel.</p>
</div>
</div>
@@ -58,16 +62,22 @@
</div>
</template>
<Profile ref="profileRef" @selected-customer="handleSelectCustomer" />
<Profile
ref="profileRef"
@change-visible="handleChangeVisible"
@selected-customer="handleSelectCustomer"
is-customer
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useGenerateStore, useUserInfoStore } from '@/stores'
import { showToast } from 'vant'
import { customerCheckin } from '@/api/workshop'
import { showToast, closeToast } from 'vant'
import { customerCheckin, createCustomer, type CreateCustomerParams } from '@/api/workshop'
import Profile from '../Workshop/profile.vue'
import MyEvent from '@/utils/myEvent'
const profileRef = ref<typeof Profile>(null)
const handleOpenProfile = () => {
@@ -76,11 +86,12 @@ const handleOpenProfile = () => {
const router = useRouter()
const generateStore = useGenerateStore()
const loading = ref(false)
type PageMode = 'form' | 'entry' | 'create'
type PageMode = 'entry' | 'form' | 'create'
const pageMode = ref<PageMode>('entry')
const formTitle = computed(() => {
return pageMode.value === 'entry' ? 'Customer ID' : 'Create Profile'
return pageMode.value === 'entry' || pageMode.value === 'form' ? 'Customer ID' : 'Create Profile'
})
const handleChangeMode = (mode: PageMode) => {
@@ -94,39 +105,63 @@ const customerData = ref({
})
const handleConfirm = async () => {
if (customerData.value.vipId === '') {
showToast({
message: 'please input name and email',
position: 'top'
})
if (loading.value) return
const nickname = (customerData.value.nickname || '').trim()
const vipId = (customerData.value.vipId || '').trim()
if (!nickname) {
showToast({ message: 'please input the nickname' })
return
}
if (pageMode.value === 'form') {
await customerCheckin({ nickname: customerData.value.nickname }).then((res) => {
useUserInfoStore().resetGenerateParams()
generateStore.setCustomerInfo(res)
router.push('/workshop/home')
})
} else {
// 创建接口
if (pageMode.value === 'create' && !vipId) {
showToast({ message: 'please input the VIP ID' })
return
}
loading.value = true
showToast({ message: 'Processing...', duration: 0, type: 'loading' })
try {
if (pageMode.value === 'create') {
await createCustomer({ nickname, vipId } as CreateCustomerParams)
showToast({ message: 'Customer created successfully' })
MyEvent.emit('update-customer-list')
}
const res = await customerCheckin({ nickname })
useUserInfoStore().resetGenerateParams()
generateStore.setCustomerInfo(res)
MyEvent.emit('clear-generate-state')
router.push('/workshop/home')
} catch (err: any) {
showToast({ message: err?.message || 'Operation failed' })
} finally {
loading.value = false
closeToast()
}
}
const handleShowPopup = (flag: Boolean) => {
// showPopup.value = flag
profileRef.value.openSwitchCustomerPopup(flag)
profileRef.value.handleShowPopup(flag,true)
}
const handleSelectCustomer = (value) => {
console.log(value)
// if (selected) {
// customerData.value.nickname = selected.nickname
// }
if (value && pageMode.value === 'form') {
customerData.value.nickname = value.name
}
}
const profileVisible = ref(false)
const handleChangeVisible = (visible: boolean) => {
profileVisible.value = visible
}
const handleBack = (e?: Event) => {
e.stopPropagation()
e.preventDefault()
if (e) {
e.stopPropagation()
e.preventDefault()
}
if (pageMode.value !== 'entry') {
pageMode.value = 'entry'
customerData.value = {
@@ -154,7 +189,7 @@ const handleBack = (e?: Event) => {
.setting {
z-index: 1;
padding: 3.17rem 4.9rem 0 8.4rem;
padding: 16.4rem 4.9rem 0 8.4rem;
font-size: 7rem;
.c-svg {
width: initial;
@@ -188,6 +223,7 @@ const handleBack = (e?: Event) => {
line-height: 8.3rem;
font-size: 4.8rem;
border: 0.2rem solid #fff;
box-sizing: border-box;
}
}
}

View File

@@ -1,15 +1,19 @@
<template>
<div class="dressfor-container flex">
<div class="content flex-1 flex flex-column">
<div class="loading-container flex flex-center">
<!-- 移除始终显示的 loading改为按需显示 -->
<!-- <div class="loading-container flex flex-center">
<Icon class="icon-element" title="" />
</div>
<div class="text">
</div> -->
<!-- <div class="text">
What are you <br />
dressing for?
</div> -->
<div class="slogan flex flex-center">
<img class="text" src="@/assets/images/dressfor.png" alt="" />
</div>
<!-- <div class="start-btn" @click="handleStart">Start</div> -->
<div class="chatbox flex flex-center">
<!-- <div class="chatbox flex flex-center">
<div class="input-box flex">
<div class="input-wrapper flex-1 flex">
<input
@@ -32,13 +36,14 @@
/>
</div>
<div class="send flex flex-center" @click="handleSendMessage">
<SvgIcon class="send-icon" name="send" size="26" color="#000000" />
<SvgIcon class="send-icon" name="send_bold" size="26" color="#6d6868" />
</div>
</div>
</div> -->
<div class="tag-container flex flex-column flex-center">
<div class="tag-list short flex flex-justify-center">
<div class="tag-list short flex">
<div
class="tag-item"
:class="{ active: item === inputValue }"
v-for="item in tagListShort"
:key="item"
@click="handleClickTag(item)"
@@ -46,33 +51,53 @@
{{ item }}
</div>
</div>
<div class="tag-list long flex flex-justify-center">
<!-- <div class="tag-list long flex flex-justify-center">
<div
class="tag-item"
v-for="item in tagListLong"
:class="{ active: item === inputValue }"
:key="item"
@click="handleClickTag(item)"
>
{{ item }}
</div>
</div>
</div> -->
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted, nextTick, watch } from 'vue'
import { useUserInfoStore, useGenerateStore } from '@/stores'
import { showToast, closeToast } from 'vant'
import { useRouter } from 'vue-router'
import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue'
import { useRouter } from 'vue-router'
import AudioVisualizer from '@/views/asistant/components/AudioVisualizer.vue'
import Icon from '../asistant/components/GenerateLoading.vue'
const router = useRouter()
const userInfoStore = useUserInfoStore()
const generateStore = useGenerateStore()
const tagListShort = [
'Casual',
'Formal',
'Activewear',
'Resort',
'Business casual',
'Evening',
'Outdoor',
'Business',
'Cocktail',
const tagListShort = ['Silk Slip Dress', 'Business Casual', 'Suggest Shoe Styles']
const tagListLong = ['Linen Suit For Summer Gaka', 'Recomment Evening Bags']
'Bridal',
'Festival',
'Travel',
'Athleisure',
'Beach',
'Ski'
]
// const tagListLong = ['Linen Suit For Summer Gaka', 'Recomment Evening Bags']
const inputValue = ref('')
const isRecording = ref(false)
@@ -101,11 +126,6 @@ const handleSendMessage = () => {
showToast('Please enter a message')
return
}
router.push({
path: '/asistant',
query: message ? { message } : undefined
})
}
const handleClickAudio = () => {
@@ -195,6 +215,16 @@ const stopRecording = () => {
const handleClickTag = (tag: string) => {
inputValue.value = tag
const sessionId = Math.floor(Date.now() / 1000).toString()
generateStore.setSessionId(sessionId.value)
// 直接跳转到 selectStyle 页面,传递消息和 sessionId
router.push({
path: '/workshop/selectStyle',
query: {
message: tag,
sessionId
}
})
}
onUnmounted(() => {
@@ -218,7 +248,21 @@ onUnmounted(() => {
background-size: cover;
background-position: center;
background-repeat: no-repeat;
padding: 6rem 0 0 0;
padding: 15.9rem 0 0 0;
.loading-container {
width: 100%;
height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
color: #000;
}
.content {
.loading-container {
:deep(.loading-image) {
@@ -227,94 +271,88 @@ onUnmounted(() => {
animation: none;
}
:deep(.loading-shadow) {
background-color: #000;
width: 9.2rem;
height: 2.4rem;
filter: blur(6px);
opacity: 0.5;
opacity: 0.2;
margin: 2.4rem 0 0;
// background-color: #d9d9d9;
}
}
.text {
font-family: 'satoshiBold';
font-size: 9.6rem;
text-align: center;
padding-top: 9rem;
padding-bottom: 14rem;
font-weight: 700;
line-height: 1.12;
background: #b3b3b3;
background: linear-gradient(120deg, #b3b3b3 1%, rgba(0, 0, 0, 0) 48%),
linear-gradient(344deg, #b3b3b2 16%, #000000 66%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.chatbox {
height: 9.3rem;
// background-color: #fff;
column-gap: 2.29rem;
.input-box {
width: 59.8rem;
height: 100%;
background-color: #efefef;
// border: 2px solid #5f5f5f;
border-radius: 1rem;
color: #222222;
font-size: 3.2rem;
font-family: 'satoshiRegular';
padding: 0 2.6rem;
column-gap: 2.6rem;
overflow: hidden;
.input-wrapper {
overflow: hidden;
}
.recording-visualizer {
display: flex;
align-items: center;
height: 100%;
:deep(.audio-visualizer) {
width: 100%;
padding: 0;
}
:deep(.visualizer-container) {
height: 100%;
}
}
.input-item {
// width: 100%;
height: 100%;
outline: none;
border: none;
background-color: #efefef;
}
.audio-icon {
width: initial;
}
}
.send {
width: 7.6rem;
height: 7.6rem;
background-color: #efefef;
border-radius: 1rem;
}
padding-top: 8.9rem;
padding-bottom: 7.7rem;
width: 60rem;
}
// .chatbox {
// height: 9.3rem;
// // background-color: #fff;
// column-gap: 2.29rem;
// .input-box {
// width: 59.8rem;
// height: 100%;
// background-color: #efefef;
// // border: 2px solid #5f5f5f;
// border-radius: 1rem;
// color: #222222;
// font-size: 3.2rem;
// font-family: 'satoshiRegular';
// padding: 0 2.6rem;
// column-gap: 2.6rem;
// overflow: hidden;
// .input-wrapper {
// overflow: hidden;
// }
// .recording-visualizer {
// display: flex;
// align-items: center;
// height: 100%;
// :deep(.audio-visualizer) {
// width: 100%;
// padding: 0;
// }
// :deep(.visualizer-container) {
// height: 100%;
// }
// }
// .input-item {
// // width: 100%;
// height: 100%;
// outline: none;
// border: none;
// background-color: #efefef;
// }
// .audio-icon {
// width: initial;
// }
// }
// .send {
// width: 7.6rem;
// height: 7.6rem;
// background-color: #efefef;
// border-radius: 1rem;
// }
// }
.tag-container {
row-gap: 3.1rem;
padding-top: 5.7rem;
// padding: 5.7rem 0;
margin: 0 auto;
width: 65.8rem;
.tag-list {
color: #000;
flex-wrap: wrap;
&.short {
column-gap: 1.91rem;
}
&.long {
column-gap: 3.1rem;
padding-left: 2.1rem;
justify-content: space-between;
align-content: flex-start;
gap: 3rem;
&::after {
content: '';
flex-grow: 1;
height: 0;
}
.tag-item {
height: 6.8rem;
min-width: 12rem;
line-height: 6.8rem;
box-sizing: border-box;
font-family: 'satoshiRegular';
@@ -323,6 +361,9 @@ onUnmounted(() => {
text-align: center;
border-radius: 4.6rem;
padding: 0 2.15rem;
&.active {
background-color: #f5f5f5;
}
}
}
}

View File

@@ -4,7 +4,8 @@
<div class="content">
<!-- 标题 -->
<div class="header">
<div class="title">CHOOSE YOUR STYLIST</div>
<div class="title">Choose Stylist.</div>
<div class="sub-title">What style are you looking for?</div>
</div>
<div class="carousel-container" v-show="!showVideo">
@@ -34,8 +35,14 @@
</div>
<!-- Continue按钮 -->
<div class="continue-button" @click="handleContinue" v-if="!$route.query?.demo">Continue</div>
<van-dialog
<button
class="sandblasted-blurred continue-button flex flex-center"
@click="handleContinue"
v-if="!$route.query?.demo"
>
<span>Continue</span>
</button>
<!-- <van-dialog
class="video-dialog"
:show-confirm-button="false"
:show-cancel-button="false"
@@ -47,7 +54,7 @@
<van-icon name="cross" class="close-icon" />
</div>
<Video ref="videoRef" />
</van-dialog>
</van-dialog> -->
</div>
</template>
@@ -57,9 +64,15 @@ import { useRouter } from 'vue-router'
import Video from './components/Video.vue'
import { useUserInfoStore } from '@/stores'
import male from '@/assets/images/male.png'
import maleThumb from '@/assets/images/male_thumb.png'
import female from '@/assets/images/female.png'
import femaleThumb from '@/assets/images/female_thumb.png'
import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue'
import mini from '@/assets/images/mini.jpg'
import miniThumb from '@/assets/images/mini_thumb.jpg'
import Crystal from '@/assets/images/Crystal.jpg'
import CrystalThumb from '@/assets/images/Crystal_thumb.jpg'
const router = useRouter()
const userInfoStore = useUserInfoStore()
@@ -68,30 +81,34 @@ const stylists = ref<any[]>([
{
id: 1,
value: 'crystal',
name: 'Vera Lo',
name: 'Crystal',
description: 'Contemporary, Classic, Simple Silhouettes, Statement Pieces',
image: female
image: Crystal,
thumb: CrystalThumb
},
{
id: 2,
value: 'mini',
name: 'Sarah Chen',
name: 'Mini',
description: 'Modern, Edgy, Bold Colors, Street Style',
image: male
image: mini,
thumb: miniThumb
},
{
id: 3,
value: 'crystal',
name: 'Emma Wilson',
value: 'vera',
name: 'Vera',
description: 'Elegant, Feminine, Vintage Inspired, Soft Tones',
image: female
image: female,
thumb: femaleThumb
},
{
id: 4,
value: 'mini',
name: 'Alex Johnson',
value: 'edi',
name: 'Edi',
description: 'Minimalist, Professional, Neutral Palette, Clean Lines',
image: male
image: male,
thumb: maleThumb
}
])
const currentChoosed = ref(1)
@@ -119,20 +136,22 @@ const handleClickStylist = (item: any) => {
const handleContinue = () => {
const generateParams = userInfoStore.getGenerateParams()
generateParams.stylist =
stylists.value.find((item) => item.id === currentChoosed.value)?.value || ''
const selected = stylists.value.find((item) => item.id === currentChoosed.value)
generateParams.stylist = selected?.value
generateParams.stylistImage = selected?.thumb
userInfoStore.setGenerateParams(generateParams)
router.push('/workshop/stylist/sex')
}
// 监听showVideo变化关闭时暂停视频
watch(showVideo, (newValue) => {
if (!newValue && videoRef.value) {
videoRef.value.pause()
videoRef.value.reset()
}
})
// watch(showVideo, (newValue) => {
// if (!newValue && videoRef.value) {
// videoRef.value.pause()
// videoRef.value.reset()
// }
// })
</script>
<style scoped lang="less">
@@ -157,18 +176,24 @@ watch(showVideo, (newValue) => {
display: flex;
flex-direction: column;
padding: 2rem;
padding-top: 6rem;
padding-top: 10rem;
}
.header {
text-align: center;
margin-bottom: 4rem;
.title {
font-size: 15rem;
font-size: 11rem;
font-weight: 400;
color: white;
font-family: 'boskaRegular';
line-height: 96%;
font-family: 'satoshiBold';
letter-spacing: -0.04rem;
margin-bottom: 3.2rem;
}
.sub-title {
font-family: 'satoshiRegular';
font-size: 4rem;
}
}
@@ -279,9 +304,9 @@ watch(showVideo, (newValue) => {
}
}
.continue-button {
height: 5.9rem;
box-sizing: border-box;
button.sandblasted-blurred.continue-button {
height: 6.7rem;
width: 24.6rem;
position: absolute;
bottom: 6.4rem;
right: 7.6rem;
@@ -291,11 +316,9 @@ watch(showVideo, (newValue) => {
color: white;
font-size: 4rem;
font-weight: 500;
cursor: pointer;
z-index: 3;
font-family: 'satoshiRegular';
display: flex;
align-items: center;
box-sizing: border-box;
}
:deep(.van-overlay) {

View File

@@ -55,14 +55,18 @@ const handleSelect = (value: string) => {
background-repeat: no-repeat;
padding: 6rem 12.4rem 0 8.5rem;
.text {
font-family: 'robotoBold';
font-size: 13rem;
font-family: 'satoshiBold';
font-weight: 700;
font-size: 11rem;
line-height: 106%;
letter-spacing: -0.02rem;
margin-bottom: 4.6rem;
}
.desc {
font-family: 'satoshiRegular';
font-size: 6.4rem;
font-size: 6rem;
line-height: 132%;
letter-spacing: 0.02rem;
}
.select-list {
display: flex;
@@ -76,7 +80,7 @@ const handleSelect = (value: string) => {
text-align: center;
font-family: 'satoshiRegular';
font-size: 4.8rem;
width: 29.7rem;
width: 35rem;
height: 8.3rem;
display: flex;
align-items: center;

View File

@@ -56,7 +56,7 @@ export default defineConfig(({ mode }) => {
},
server: {
host: '0.0.0.0', // 允许局域网内的IP访问
port: 8060, // 根据环境设置端口
port: 8066, // 根据环境设置端口
open: false, // 自动打开浏览器
strictPort: true, // 如果端口已被占用,则尝试下一个可用端口
hmr: {