1轮修复
This commit is contained in:
64
urbanLifelineServ/ai/src/main/resources/bootstrap.yml
Normal file
64
urbanLifelineServ/ai/src/main/resources/bootstrap.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
# ================================================
|
||||
# Urban Lifeline - 通用 Bootstrap 配置
|
||||
# 所有微服务共享的基础配置
|
||||
# ================================================
|
||||
|
||||
# ================== Spring Cloud Nacos ==================
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
namespace: ${NACOS_NAMESPACE:dev}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
|
||||
# ================== DataSource ==================
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:postgresql://127.0.0.1:5432/urban_lifeline}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:postgres}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
# ================== Redis ==================
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:127.0.0.1}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
password: ${REDIS_PASSWORD:123456}
|
||||
|
||||
# ================== Security AES ==================
|
||||
security:
|
||||
aes:
|
||||
# AES-256 密钥(Base64编码,必须与所有服务保持一致)
|
||||
# 警告:这是开发环境密钥,生产环境请使用密钥管理系统
|
||||
secret-key: ${AES_SECRET_KEY:MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=}
|
||||
|
||||
# ================== Dubbo ==================
|
||||
dubbo:
|
||||
protocol:
|
||||
name: dubbo
|
||||
port: -1
|
||||
registry:
|
||||
address: nacos://${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
|
||||
# ================== MyBatis-Plus ==================
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/**/*.xml
|
||||
type-aliases-package: org.xyzh.common.dto, org.xyzh.api
|
||||
|
||||
# ================== SpringDoc 基础配置 ==================
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
path: /swagger-ui.html
|
||||
|
||||
# ================== Logging ==================
|
||||
logging:
|
||||
config: classpath:log4j2.xml
|
||||
charset:
|
||||
console: UTF-8
|
||||
file: UTF-8
|
||||
64
urbanLifelineServ/auth/src/main/resources/bootstrap.yml
Normal file
64
urbanLifelineServ/auth/src/main/resources/bootstrap.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
# ================================================
|
||||
# Urban Lifeline - 通用 Bootstrap 配置
|
||||
# 所有微服务共享的基础配置
|
||||
# ================================================
|
||||
|
||||
# ================== Spring Cloud Nacos ==================
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
namespace: ${NACOS_NAMESPACE:dev}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
|
||||
# ================== DataSource ==================
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:postgresql://127.0.0.1:5432/urban_lifeline}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:postgres}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
# ================== Redis ==================
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:127.0.0.1}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
password: ${REDIS_PASSWORD:123456}
|
||||
|
||||
# ================== Security AES ==================
|
||||
security:
|
||||
aes:
|
||||
# AES-256 密钥(Base64编码,必须与所有服务保持一致)
|
||||
# 警告:这是开发环境密钥,生产环境请使用密钥管理系统
|
||||
secret-key: ${AES_SECRET_KEY:MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=}
|
||||
|
||||
# ================== Dubbo ==================
|
||||
dubbo:
|
||||
protocol:
|
||||
name: dubbo
|
||||
port: -1
|
||||
registry:
|
||||
address: nacos://${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
|
||||
# ================== MyBatis-Plus ==================
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/**/*.xml
|
||||
type-aliases-package: org.xyzh.common.dto, org.xyzh.api
|
||||
|
||||
# ================== SpringDoc 基础配置 ==================
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
path: /swagger-ui.html
|
||||
|
||||
# ================== Logging ==================
|
||||
logging:
|
||||
config: classpath:log4j2.xml
|
||||
charset:
|
||||
console: UTF-8
|
||||
file: UTF-8
|
||||
64
urbanLifelineServ/file/src/main/resources/bootstrap.yml
Normal file
64
urbanLifelineServ/file/src/main/resources/bootstrap.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
# ================================================
|
||||
# Urban Lifeline - 通用 Bootstrap 配置
|
||||
# 所有微服务共享的基础配置
|
||||
# ================================================
|
||||
|
||||
# ================== Spring Cloud Nacos ==================
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
namespace: ${NACOS_NAMESPACE:dev}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
|
||||
# ================== DataSource ==================
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:postgresql://127.0.0.1:5432/urban_lifeline}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:postgres}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
# ================== Redis ==================
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:127.0.0.1}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
password: ${REDIS_PASSWORD:123456}
|
||||
|
||||
# ================== Security AES ==================
|
||||
security:
|
||||
aes:
|
||||
# AES-256 密钥(Base64编码,必须与所有服务保持一致)
|
||||
# 警告:这是开发环境密钥,生产环境请使用密钥管理系统
|
||||
secret-key: ${AES_SECRET_KEY:MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=}
|
||||
|
||||
# ================== Dubbo ==================
|
||||
dubbo:
|
||||
protocol:
|
||||
name: dubbo
|
||||
port: -1
|
||||
registry:
|
||||
address: nacos://${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
|
||||
# ================== MyBatis-Plus ==================
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/**/*.xml
|
||||
type-aliases-package: org.xyzh.common.dto, org.xyzh.api
|
||||
|
||||
# ================== SpringDoc 基础配置 ==================
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
path: /swagger-ui.html
|
||||
|
||||
# ================== Logging ==================
|
||||
logging:
|
||||
config: classpath:log4j2.xml
|
||||
charset:
|
||||
console: UTF-8
|
||||
file: UTF-8
|
||||
@@ -10,6 +10,7 @@ import com.alibaba.fastjson2.JSON;
|
||||
import org.apache.dubbo.config.annotation.DubboReference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
@@ -81,7 +82,7 @@ public class GuestController {
|
||||
|
||||
|
||||
@PostMapping
|
||||
public ResultDomain<TbGuestDTO> createGuest(TbGuestDTO guest) {
|
||||
public ResultDomain<TbGuestDTO> createGuest(@RequestBody TbGuestDTO guest) {
|
||||
ValidationUtils.validate(guest, Arrays.asList(
|
||||
ValidationUtils.requiredString("name", "姓名"),
|
||||
ValidationUtils.atLeastOne(
|
||||
@@ -93,7 +94,7 @@ public class GuestController {
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public ResultDomain<TbGuestDTO> updateGuest(TbGuestDTO guest) {
|
||||
public ResultDomain<TbGuestDTO> updateGuest(@RequestBody TbGuestDTO guest) {
|
||||
ValidationUtils.validate(guest, Arrays.asList(
|
||||
ValidationUtils.requiredString("name", "姓名"),
|
||||
ValidationUtils.atLeastOne(
|
||||
@@ -104,8 +105,8 @@ public class GuestController {
|
||||
return guestService.updateGuest(guest);
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResultDomain<TbGuestDTO> deleteGuest(@NotNull String userId) {
|
||||
@DeleteMapping("/{userId}")
|
||||
public ResultDomain<TbGuestDTO> deleteGuest(@PathVariable("userId") @NotNull String userId) {
|
||||
return guestService.deleteGuest(userId);
|
||||
}
|
||||
|
||||
@@ -115,13 +116,13 @@ public class GuestController {
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/list")
|
||||
public ResultDomain<TbGuestDTO> listGuest(TbGuestDTO filter) {
|
||||
@PostMapping("/list")
|
||||
public ResultDomain<TbGuestDTO> listGuest(@RequestBody TbGuestDTO filter) {
|
||||
return guestService.selectGuestList(filter);
|
||||
}
|
||||
|
||||
@PostMapping("/page")
|
||||
public ResultDomain<TbGuestDTO> pageGuest(PageRequest<TbGuestDTO> pageRequest) {
|
||||
public ResultDomain<TbGuestDTO> pageGuest(@RequestBody PageRequest<TbGuestDTO> pageRequest) {
|
||||
return guestService.selectGuestPage(pageRequest);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.xyzh.system.mapper.user;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.security.access.method.P;
|
||||
import org.xyzh.common.core.page.PageParam;
|
||||
import org.xyzh.common.dto.sys.TbGuestDTO;
|
||||
|
||||
@@ -64,7 +66,7 @@ public interface TbGuestMapper extends BaseMapper<TbGuestDTO>{
|
||||
* @author yslg
|
||||
* @since 2025-12-18
|
||||
*/
|
||||
List<TbGuestDTO> selectGuestList(TbGuestDTO guest);
|
||||
List<TbGuestDTO> selectGuestList(@Param("filter") TbGuestDTO filter);
|
||||
|
||||
/**
|
||||
* @description 查询来客分页列表
|
||||
@@ -73,7 +75,7 @@ public interface TbGuestMapper extends BaseMapper<TbGuestDTO>{
|
||||
* @author yslg
|
||||
* @since 2025-12-18
|
||||
*/
|
||||
List<TbGuestDTO> selectGuestPage(TbGuestDTO guest, PageParam pageParam);
|
||||
List<TbGuestDTO> selectGuestPage(@Param("filter") TbGuestDTO guest, @Param("pageParam") PageParam pageParam);
|
||||
|
||||
int countGuest(TbGuestDTO guest);
|
||||
}
|
||||
|
||||
64
urbanLifelineServ/system/src/main/resources/bootstrap.yml
Normal file
64
urbanLifelineServ/system/src/main/resources/bootstrap.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
# ================================================
|
||||
# Urban Lifeline - 通用 Bootstrap 配置
|
||||
# 所有微服务共享的基础配置
|
||||
# ================================================
|
||||
|
||||
# ================== Spring Cloud Nacos ==================
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
namespace: ${NACOS_NAMESPACE:dev}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
|
||||
# ================== DataSource ==================
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:postgresql://127.0.0.1:5432/urban_lifeline}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:postgres}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
# ================== Redis ==================
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:127.0.0.1}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
password: ${REDIS_PASSWORD:123456}
|
||||
|
||||
# ================== Security AES ==================
|
||||
security:
|
||||
aes:
|
||||
# AES-256 密钥(Base64编码,必须与所有服务保持一致)
|
||||
# 警告:这是开发环境密钥,生产环境请使用密钥管理系统
|
||||
secret-key: ${AES_SECRET_KEY:MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=}
|
||||
|
||||
# ================== Dubbo ==================
|
||||
dubbo:
|
||||
protocol:
|
||||
name: dubbo
|
||||
port: -1
|
||||
registry:
|
||||
address: nacos://${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
|
||||
# ================== MyBatis-Plus ==================
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/**/*.xml
|
||||
type-aliases-package: org.xyzh.common.dto, org.xyzh.api
|
||||
|
||||
# ================== SpringDoc 基础配置 ==================
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
path: /swagger-ui.html
|
||||
|
||||
# ================== Logging ==================
|
||||
logging:
|
||||
config: classpath:log4j2.xml
|
||||
charset:
|
||||
console: UTF-8
|
||||
file: UTF-8
|
||||
@@ -62,7 +62,15 @@ public class WorkcaseController {
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
return workcaseService.createWorkcase(workcase);
|
||||
try{
|
||||
workcaseService.createWorkcase(workcase);
|
||||
return ResultDomain.success("创建工单成功", workcase);
|
||||
} catch (Exception e) {
|
||||
if(e.getMessage().contains("tb_workcase_room_id_key")){
|
||||
return ResultDomain.failure("您已有未完成的工单,请勿重复提交");
|
||||
}
|
||||
return ResultDomain.failure("创建工单失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "更新工单")
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.xyzh.api.file.dto.TbSysFileDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
|
||||
import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO;
|
||||
@@ -76,6 +77,7 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
||||
// ====================== 工单管理 ======================
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultDomain<TbWorkcaseDTO> createWorkcase(TbWorkcaseDTO workcase) {
|
||||
logger.info("创建工单: userId={}, type={}, roomId={}", workcase.getUserId(), workcase.getType(), workcase.getRoomId());
|
||||
|
||||
|
||||
64
urbanLifelineServ/workcase/src/main/resources/bootstrap.yml
Normal file
64
urbanLifelineServ/workcase/src/main/resources/bootstrap.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
# ================================================
|
||||
# Urban Lifeline - 通用 Bootstrap 配置
|
||||
# 所有微服务共享的基础配置
|
||||
# ================================================
|
||||
|
||||
# ================== Spring Cloud Nacos ==================
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
namespace: ${NACOS_NAMESPACE:dev}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
|
||||
# ================== DataSource ==================
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:postgresql://127.0.0.1:5432/urban_lifeline}
|
||||
username: ${DB_USERNAME:postgres}
|
||||
password: ${DB_PASSWORD:postgres}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
# ================== Redis ==================
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:127.0.0.1}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
password: ${REDIS_PASSWORD:123456}
|
||||
|
||||
# ================== Security AES ==================
|
||||
security:
|
||||
aes:
|
||||
# AES-256 密钥(Base64编码,必须与所有服务保持一致)
|
||||
# 警告:这是开发环境密钥,生产环境请使用密钥管理系统
|
||||
secret-key: ${AES_SECRET_KEY:MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=}
|
||||
|
||||
# ================== Dubbo ==================
|
||||
dubbo:
|
||||
protocol:
|
||||
name: dubbo
|
||||
port: -1
|
||||
registry:
|
||||
address: nacos://${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||
|
||||
# ================== MyBatis-Plus ==================
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/**/*.xml
|
||||
type-aliases-package: org.xyzh.common.dto, org.xyzh.api
|
||||
|
||||
# ================== SpringDoc 基础配置 ==================
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
path: /swagger-ui.html
|
||||
|
||||
# ================== Logging ==================
|
||||
logging:
|
||||
config: classpath:log4j2.xml
|
||||
charset:
|
||||
console: UTF-8
|
||||
file: UTF-8
|
||||
26
urbanLifelineWeb/package-lock.json
generated
26
urbanLifelineWeb/package-lock.json
generated
@@ -1847,12 +1847,6 @@
|
||||
"resolved": "packages/workcase",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@vant/weapp": {
|
||||
"version": "1.11.7",
|
||||
"resolved": "https://registry.npmmirror.com/@vant/weapp/-/weapp-1.11.7.tgz",
|
||||
"integrity": "sha512-Rwn9BBnb4kHSV4XmvBicwtd42J+amEUfnFDcXJsGNPNX4a9c/DoT6YLsm4X1wB2+sQbdiQsbFBLAvGRBxCkD8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
@@ -5038,10 +5032,6 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/taihao-service-miniprogram": {
|
||||
"resolved": "packages/wechat_demo",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -5509,12 +5499,14 @@
|
||||
"@vueuse/core": "^11.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"element-plus": "^2.12.0",
|
||||
"lucide-vue-next": "^0.561.0",
|
||||
"pinia": "^2.2.8",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@module-federation/vite": "^1.9.3",
|
||||
"@types/node": "^20.10.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"typescript": "^5.7.2",
|
||||
@@ -5522,6 +5514,16 @@
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"packages/bidding/node_modules/@types/node": {
|
||||
"version": "20.19.30",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.30.tgz",
|
||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"packages/platform": {
|
||||
"name": "@urbanlifeline/platform",
|
||||
"version": "1.0.0",
|
||||
@@ -5551,6 +5553,7 @@
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@stomp/stompjs": "^7.2.1",
|
||||
"axios": "^1.7.0",
|
||||
"cors": "^2.8.5",
|
||||
"element-plus": "^2.12.0",
|
||||
"express": "^4.18.2",
|
||||
@@ -7031,6 +7034,7 @@
|
||||
"packages/wechat_demo": {
|
||||
"name": "taihao-service-miniprogram",
|
||||
"version": "1.0.0",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"@vant/weapp": "^1.11.7"
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const guestAPI = {
|
||||
* 获取来客列表
|
||||
*/
|
||||
async listGuest(filter?: TbGuestDTO): Promise<ResultDomain<TbGuestDTO>> {
|
||||
const response = await api.get<TbGuestDTO>(`${this.baseUrl}/list`, { params: filter })
|
||||
const response = await api.post<TbGuestDTO>(`${this.baseUrl}/list`, { params: filter })
|
||||
return response.data
|
||||
},
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export default defineConfig({
|
||||
'./api/auth': './src/api/auth/auth.ts',
|
||||
'./api/file': './src/api/file/file.ts',
|
||||
'./api/ai': './src/api/ai/index.ts',
|
||||
'./api/sys/guest': './src/api/sys/guest.ts',
|
||||
|
||||
// ========== Utils 工具模块 ==========
|
||||
'./utils': './src/utils/index.ts',
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export * from './workcase'
|
||||
export * from './workcase'
|
||||
|
||||
// 从shared导入系统和聊天相关API
|
||||
export { guestAPI as systemAPI } from 'shared/api/sys/guest'
|
||||
export { workcaseChatAPI as chatAPI } from './workcase'
|
||||
@@ -77,6 +77,10 @@ declare module 'shared/api/ai' {
|
||||
export const aiChatAPI: any
|
||||
}
|
||||
|
||||
declare module 'shared/api/sys/guest' {
|
||||
export const guestAPI: any
|
||||
}
|
||||
|
||||
// ============ types模块 ==================
|
||||
declare module 'shared/types' {
|
||||
// 基础类型
|
||||
@@ -117,7 +121,7 @@ declare module 'shared/types' {
|
||||
export type { LoginParam, LoginDomain } from '../../../shared/src/types/auth'
|
||||
|
||||
// 重新导出 sys
|
||||
export type { SysUserVO, SysConfigVO, TbSysViewDTO } from '../../../shared/src/types/sys'
|
||||
export type { SysUserVO, SysConfigVO, TbSysViewDTO, TbGuestDTO } from '../../../shared/src/types/sys'
|
||||
|
||||
// 重新导出 file
|
||||
export type { TbSysFileDTO } from '../../../shared/src/types/file'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<AdminLayout title="工单管理" info="查看和处理客户服务工单">
|
||||
<!-- <template #action>
|
||||
<template #action>
|
||||
<el-button type="primary" @click="showCreateDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建工单
|
||||
</el-button>
|
||||
</template> -->
|
||||
</template>
|
||||
|
||||
<div class="workcase-container">
|
||||
<!-- 筛选区域 -->
|
||||
@@ -97,18 +97,42 @@
|
||||
<!-- 创建工单弹窗 -->
|
||||
<el-dialog v-model="showCreateDialog" title="创建工单" width="650px">
|
||||
<el-form :model="formData" label-width="90px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户姓名" required>
|
||||
<el-input v-model="formData.username" placeholder="请输入客户姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话" required>
|
||||
<el-input v-model="formData.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="来客人员" required>
|
||||
<el-select
|
||||
v-model="selectedGuestId"
|
||||
placeholder="请选择来客人员"
|
||||
style="width: 100%;"
|
||||
:loading="loadingGuests"
|
||||
filterable
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="guest in guestsList"
|
||||
:key="guest.userId"
|
||||
:label="`${guest.name} (${guest.phone || '无电话'})`"
|
||||
:value="guest.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="聊天室" required>
|
||||
<el-select
|
||||
v-model="formData.roomId"
|
||||
placeholder="请选择聊天室"
|
||||
style="width: 100%;"
|
||||
:loading="loadingChatRooms"
|
||||
filterable
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="room in chatRoomsList"
|
||||
:key="room.roomId"
|
||||
:label="`聊天室 ${room.roomName}`"
|
||||
:value="room.roomId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="故障类型" required>
|
||||
@@ -131,6 +155,13 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="设备代码" required>
|
||||
<el-input v-model="formData.deviceCode" placeholder="请输入设备代码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="设备型号" required>
|
||||
<el-select v-model="formData.device" placeholder="请选择设备型号" style="width: 100%;">
|
||||
<el-option label="TH-500GF" value="TH-500GF" />
|
||||
@@ -172,15 +203,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Plus, Search } from 'lucide-vue-next'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { workcaseAPI } from '@/api/workcase'
|
||||
import { workcaseAPI, chatAPI } from '@/api'
|
||||
import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue'
|
||||
import { WorkcaseAssign } from '@/components'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO } from '@/types/workcase'
|
||||
import type { PageRequest, PageParam } from 'shared/types'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO, TbChatRoomDTO } from '@/types/workcase'
|
||||
import type { PageRequest, PageParam, TbGuestDTO } from 'shared/types'
|
||||
import { guestAPI } from 'shared/api/sys/guest'
|
||||
|
||||
const statusFilter = ref('all')
|
||||
const typeFilter = ref('')
|
||||
@@ -201,8 +233,12 @@ const assignCurrentProcessor = ref('')
|
||||
|
||||
const formData = ref<TbWorkcaseDTO>({
|
||||
username: '',
|
||||
userId: '',
|
||||
phone: '',
|
||||
device: '',
|
||||
deviceCode: '',
|
||||
roomId: '',
|
||||
deviceNamePlate: '',
|
||||
type: '',
|
||||
emergency: 'normal',
|
||||
description: ''
|
||||
@@ -210,6 +246,97 @@ const formData = ref<TbWorkcaseDTO>({
|
||||
|
||||
const workcaseList = ref<TbWorkcaseDTO[]>([])
|
||||
|
||||
// 来客人员相关
|
||||
const guestsList = ref<TbGuestDTO[]>([])
|
||||
const loadingGuests = ref(false)
|
||||
const selectedGuestId = ref('')
|
||||
|
||||
// 聊天室相关
|
||||
const chatRoomsList = ref<TbChatRoomDTO[]>([])
|
||||
const loadingChatRooms = ref(false)
|
||||
|
||||
// 加载来客人员列表
|
||||
const loadGuests = async () => {
|
||||
loadingGuests.value = true
|
||||
try {
|
||||
const res = await guestAPI.listGuest()
|
||||
if (res.success) {
|
||||
guestsList.value = res.dataList || []
|
||||
} else {
|
||||
ElMessage.error(res.message || '加载来客人员失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载来客人员失败')
|
||||
} finally {
|
||||
loadingGuests.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 根据来客ID加载聊天室列表
|
||||
const loadChatRoomsByGuest = async (guestId: string) => {
|
||||
if (!guestId) {
|
||||
chatRoomsList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loadingChatRooms.value = true
|
||||
try {
|
||||
const filter = {
|
||||
guestId: guestId
|
||||
}
|
||||
const pageRequest = {
|
||||
filter,
|
||||
pageParam: {
|
||||
page: 1,
|
||||
pageSize: 100
|
||||
}
|
||||
}
|
||||
const res = await chatAPI.getChatRoomPage(pageRequest)
|
||||
if (res.success) {
|
||||
chatRoomsList.value = res.dataList || res.pageDomain?.dataList || []
|
||||
} else {
|
||||
ElMessage.error(res.message || '加载聊天室列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载聊天室列表失败')
|
||||
} finally {
|
||||
loadingChatRooms.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听来客人员选择变化,加载对应的聊天室列表
|
||||
watch(selectedGuestId, (newGuestId) => {
|
||||
const selectedGuest = guestsList.value.find(guest => guest.userId === newGuestId)
|
||||
if (selectedGuest) {
|
||||
formData.value.username = selectedGuest.name
|
||||
formData.value.userId = selectedGuest.userId
|
||||
formData.value.phone = selectedGuest.phone
|
||||
// 加载该来客的聊天室列表
|
||||
loadChatRoomsByGuest(selectedGuest.userId)
|
||||
} else {
|
||||
formData.value.username = ''
|
||||
formData.value.userId = ''
|
||||
formData.value.phone = ''
|
||||
chatRoomsList.value = []
|
||||
}
|
||||
})
|
||||
|
||||
// 监听弹窗打开状态,加载来客人员列表
|
||||
watch(showCreateDialog, (newVal) => {
|
||||
if (newVal) {
|
||||
// 弹窗打开时,加载来客人员列表
|
||||
loadGuests()
|
||||
} else {
|
||||
// 弹窗关闭时,重置相关数据
|
||||
selectedGuestId.value = ''
|
||||
formData.value.username = ''
|
||||
formData.value.userId = ''
|
||||
formData.value.phone = ''
|
||||
formData.value.roomId = ''
|
||||
chatRoomsList.value = []
|
||||
}
|
||||
})
|
||||
|
||||
// ========================= API 调用方法 =========================
|
||||
/**
|
||||
* 加载工单列表
|
||||
@@ -253,10 +380,11 @@ const loadWorkcases = async () => {
|
||||
* 创建工单 - API调用
|
||||
*/
|
||||
const createWorkcaseAPI = async () => {
|
||||
if (!formData.value.username || !formData.value.phone || !formData.value.device || !formData.value.type) {
|
||||
if (!formData.value.username || !formData.value.phone || !formData.value.deviceCode || !formData.value.type || !formData.value.roomId) {
|
||||
ElMessage.error('请填写必填项')
|
||||
return
|
||||
}
|
||||
formData.value.deviceNamePlate = formData.value.deviceCode; // 将设备代码赋值给设备铭牌字段
|
||||
|
||||
const res = await workcaseAPI.createWorkcase(formData.value)
|
||||
if (res.success) {
|
||||
|
||||
@@ -21,5 +21,16 @@ export const guestAPI = {
|
||||
url: `/urban-lifeline/system/guest/wechat/${wechatId}`,
|
||||
method: 'GET'
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取可选人员列表
|
||||
*/
|
||||
listGuest(filter?: TbGuestDTO): Promise<ResultDomain<TbGuestDTO>> {
|
||||
return request<TbGuestDTO>({
|
||||
url: '/urban-lifeline/system/guest/list',
|
||||
method: 'POST',
|
||||
data: filter||{}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
export const AGENT_ID = '17678420499370001'
|
||||
// export const BASE_URL = 'http://localhost:8180'
|
||||
export const BASE_URL = 'http://localhost:8180'
|
||||
// 根据宝塔nginx配置,/urban-lifeline/ 会代理到后端网关
|
||||
export const BASE_URL = 'https://demo-urbanlifeline.tensorgrove.com/urban-lifeline'
|
||||
// export const BASE_URL = 'https://demo-urbanlifeline.tensorgrove.com/urban-lifeline'
|
||||
|
||||
// export const WS_HOST = 'localhost:8180' // WebSocket host(不包含协议)
|
||||
export const WS_HOST = 'demo-urbanlifeline.tensorgrove.com/urban-lifeline' // WebSocket host(不包含协议)
|
||||
export const MEET_URL = 'https://org.xyzh.yslg/workcase'
|
||||
// export const MEET_URL = 'https://demo-urbanlifeline.tensorgrove.com/workcase'
|
||||
|
||||
export const WS_HOST = 'localhost:8180' // WebSocket host(不包含协议)
|
||||
// export const WS_HOST = 'demo-urbanlifeline.tensorgrove.com/urban-lifeline' // WebSocket host(不包含协议)
|
||||
@@ -97,6 +97,13 @@
|
||||
<view class="message-content" v-else>
|
||||
<view class="bubble other-bubble">
|
||||
<rich-text :nodes="renderMarkdown(msg.content || '')" class="message-rich-text"></rich-text>
|
||||
<!-- 文件列表 -->
|
||||
<view v-if="msg.files && msg.files.length > 0" class="message-files">
|
||||
<view v-for="fileId in msg.files" :key="fileId" class="message-file-item" @tap="downloadFile(fileId)">
|
||||
<text class="file-icon">📎</text>
|
||||
<text class="file-name">{{ getFileNameFromId(fileId) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
||||
</view>
|
||||
@@ -112,6 +119,13 @@
|
||||
<view class="message-content" v-else>
|
||||
<view class="bubble self-bubble">
|
||||
<text class="message-text">{{ msg.content }}</text>
|
||||
<!-- 文件列表 -->
|
||||
<view v-if="msg.files && msg.files.length > 0" class="message-files">
|
||||
<view v-for="fileId in msg.files" :key="fileId" class="message-file-item" @tap="downloadFile(fileId)">
|
||||
<text class="file-icon">📎</text>
|
||||
<text class="file-name">{{ getFileNameFromId(fileId) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
|
||||
</view>
|
||||
@@ -126,10 +140,29 @@
|
||||
|
||||
<!-- 底部输入区 -->
|
||||
<view class="footer">
|
||||
<!-- 文件预览区域 -->
|
||||
<view v-if="selectedFiles.length > 0" class="file-preview-container">
|
||||
<view v-for="(file, index) in selectedFiles" :key="index" class="file-preview-item">
|
||||
<view class="file-info">
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
<text class="file-size">{{ formatFileSize(file.size) }}</text>
|
||||
</view>
|
||||
<view class="file-actions">
|
||||
<text class="file-delete" @tap="removeFile(index)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 输入和操作行 -->
|
||||
<view class="input-row">
|
||||
<view class="input-actions">
|
||||
<view class="action-btn" @tap="chooseFile">
|
||||
<text class="action-icon">📎</text>
|
||||
</view>
|
||||
</view>
|
||||
<input class="chat-input" v-model="inputText" placeholder="输入消息..."
|
||||
@confirm="sendMessage" />
|
||||
<view class="send-btn" @tap="sendMessage">
|
||||
<view class="send-btn" @tap="sendMessage" :class="{ disabled: isUploading || selectedFiles.length === 0 && !inputText.trim() }">
|
||||
<text class="send-icon">➤</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -148,6 +181,7 @@ import MeetingCard from '../../meeting/meetingCard/MeetingCard.uvue'
|
||||
import CommentMessageCard from './CommentMessageCard/CommentMessageCard.uvue'
|
||||
import type { ChatRoomMessageVO, CustomerVO, ChatMemberVO, TbChatRoomMessageDTO, VideoMeetingVO } from '@/types/workcase'
|
||||
import { workcaseChatAPI } from '@/api/workcase'
|
||||
import { fileAPI } from '@/api/file'
|
||||
import { wsClient } from '@/utils/websocket'
|
||||
import { WS_HOST } from '@/config'
|
||||
// 响应式数据
|
||||
@@ -167,6 +201,11 @@ const loadingMore = ref<boolean>(false)
|
||||
const currentPage = ref<number>(1)
|
||||
const hasMore = ref<boolean>(true)
|
||||
|
||||
// 文件上传相关变量
|
||||
const uploadingFiles = ref<any[]>([])
|
||||
const selectedFiles = ref<any[]>([])
|
||||
const isUploading = ref<boolean>(false)
|
||||
|
||||
// 用户信息(从storage获取)
|
||||
const currentUserId = ref<string>('')
|
||||
const currentUserName = ref<string>('我')
|
||||
@@ -516,7 +555,7 @@ function renderMarkdown(text: string): string {
|
||||
// 发送消息
|
||||
async function sendMessage() {
|
||||
const text = inputText.value.trim()
|
||||
if (!text || sending.value) return
|
||||
if ((!text && selectedFiles.value.length === 0) || sending.value || isUploading.value) return
|
||||
|
||||
sending.value = true
|
||||
const tempId = Date.now().toString()
|
||||
@@ -530,13 +569,17 @@ async function sendMessage() {
|
||||
senderName: currentUserName.value,
|
||||
content: text,
|
||||
sendTime: new Date().toISOString(),
|
||||
status: 'sending'
|
||||
status: 'sending',
|
||||
files: []
|
||||
}
|
||||
messages.push(tempMsg)
|
||||
inputText.value = ''
|
||||
nextTick(() => scrollToBottom())
|
||||
|
||||
try {
|
||||
// 上传文件
|
||||
const fileIds = await uploadFiles()
|
||||
|
||||
// 调用API发送消息
|
||||
const msgDTO: TbChatRoomMessageDTO = {
|
||||
roomId: roomId.value,
|
||||
@@ -544,7 +587,8 @@ async function sendMessage() {
|
||||
senderType: 'guest',
|
||||
senderName: currentUserName.value,
|
||||
messageType: 'text',
|
||||
content: text
|
||||
content: text,
|
||||
files: fileIds
|
||||
}
|
||||
const res = await workcaseChatAPI.sendMessage(msgDTO)
|
||||
if (res.success && res.data) {
|
||||
@@ -734,6 +778,140 @@ async function handleCommentSubmit(rating: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// 文件操作相关函数
|
||||
|
||||
// 选择文件
|
||||
async function chooseFile() {
|
||||
try {
|
||||
const res = await uni.chooseMessageFile({
|
||||
count: 9, // 最多选择9个文件
|
||||
type: 'file',
|
||||
ext: ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt', 'jpg', 'jpeg', 'png', 'gif']
|
||||
})
|
||||
|
||||
if (res.tempFiles && res.tempFiles.length > 0) {
|
||||
// 添加到选中文件列表
|
||||
selectedFiles.value = [...selectedFiles.value, ...res.tempFiles]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('选择文件失败:', error)
|
||||
uni.showToast({ title: '选择文件失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 删除选中的文件
|
||||
function removeFile(index: number) {
|
||||
selectedFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(size: number): string {
|
||||
if (size < 1024) {
|
||||
return size + ' B'
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(1) + ' KB'
|
||||
} else {
|
||||
return (size / (1024 * 1024)).toFixed(1) + ' MB'
|
||||
}
|
||||
}
|
||||
|
||||
// 批量上传文件
|
||||
async function uploadFiles(): Promise<string[]> {
|
||||
const fileIds: string[] = []
|
||||
if (selectedFiles.value.length === 0) return fileIds
|
||||
|
||||
isUploading.value = true
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
try {
|
||||
// 获取文件路径数组
|
||||
const filePaths = selectedFiles.value.map(file => file.path)
|
||||
// 使用fileAPI的批量上传方法
|
||||
const result = await fileAPI.batchUploadFiles(filePaths, {
|
||||
module: 'chatroom'
|
||||
})
|
||||
|
||||
if (result.success && result.dataList) {
|
||||
// 提取fileId数组
|
||||
fileIds.push(...result.dataList.map(file => file.fileId!))
|
||||
} else {
|
||||
throw new Error(result.message || '文件上传失败')
|
||||
}
|
||||
|
||||
return fileIds
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error)
|
||||
uni.showToast({ title: '文件上传失败', icon: 'none' })
|
||||
throw error
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
uni.hideLoading()
|
||||
selectedFiles.value = [] // 清空选中文件列表
|
||||
}
|
||||
}
|
||||
|
||||
// 文件下载和预览功能
|
||||
|
||||
// 从fileId中提取文件名
|
||||
function getFileNameFromId(fileId: string): string {
|
||||
// 简化实现,实际项目中可以从fileId解析或通过API获取文件名
|
||||
return fileId.split('/').pop() || fileId
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
async function downloadFile(fileId: string) {
|
||||
try {
|
||||
// 使用fileAPI获取正确的文件下载URL
|
||||
const downloadUrl = fileAPI.getDownloadUrl(fileId)
|
||||
|
||||
// 发起下载请求
|
||||
uni.downloadFile({
|
||||
url: downloadUrl,
|
||||
header: {
|
||||
Authorization: `Bearer ${uni.getStorageSync('token') || ''}`
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
// 保存文件到本地
|
||||
uni.saveFile({
|
||||
filePath: res.tempFilePath,
|
||||
success: (saveRes) => {
|
||||
uni.showToast({
|
||||
title: '文件已保存',
|
||||
icon: 'success'
|
||||
})
|
||||
// 打开文件
|
||||
uni.openDocument({
|
||||
filePath: saveRes.savedFilePath,
|
||||
success: () => {
|
||||
console.log('文件打开成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '文件下载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('文件下载失败:', error)
|
||||
uni.showToast({
|
||||
title: '文件下载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('文件下载异常:', error)
|
||||
uni.showToast({
|
||||
title: '文件下载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
@@ -822,4 +1000,166 @@ function handleNewMessage(message: ChatRoomMessageVO) {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./chatRoom.scss";
|
||||
|
||||
// 文件上传相关样式
|
||||
.file-preview-container {
|
||||
padding: 10px;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.file-preview-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: white;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #e8e8e8;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.file-delete {
|
||||
font-size: 12px;
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 77, 79, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 输入区操作按钮
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// 发送按钮状态
|
||||
.send-btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
// 消息中的文件列表
|
||||
.message-files {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message-file-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.message-file-item .file-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.message-file-item .file-name {
|
||||
font-size: 13px;
|
||||
color: #1890ff;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// 适配自己消息的文件样式
|
||||
.self .message-files {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.self .message-file-item {
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
// 适配对方消息的文件样式
|
||||
.other .message-files {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.other .message-file-item {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,293 +2,745 @@
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: navPaddingTop + 'px', height: navHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<view class="nav-back-icon"></view>
|
||||
</view>
|
||||
<text class="nav-title">我的聊天室</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天室列表 -->
|
||||
<scroll-view class="list" scroll-y="true" :style="{ marginTop: navHeight + 'px' }">
|
||||
<view class="room-card" v-for="(room, index) in chatRooms" :key="index" @tap="enterRoom(room)">
|
||||
<view class="room-avatar">
|
||||
<text class="avatar-text">{{ room.guestName?.charAt(0) || '客' }}</text>
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: navPaddingTop + 'px', height: navTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<view class="nav-back-icon"></view>
|
||||
</view>
|
||||
<view class="room-info">
|
||||
<view class="room-header">
|
||||
<text class="room-name">{{ room.roomName || '聊天室' }}</text>
|
||||
<text class="room-time">{{ formatTime(room.lastMessageTime) }}</text>
|
||||
<text class="nav-title">我的聊天室</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
|
||||
<!-- 用户筛选组件(仅非guest用户可见) -->
|
||||
<view class="filter-section" v-if="!isGuest" :style="{ marginTop: navTotalHeight + 'px' }">
|
||||
<view class="filter-label">选择用户:</view>
|
||||
<view class="filter-content">
|
||||
<view class="guest-selector" @tap="showGuestSelector = true">
|
||||
<text v-if="selectedGuestId" class="selected-guest">
|
||||
{{ guestsList.find(g => g.userId === selectedGuestId)?.name || '请选择用户' }}
|
||||
</text>
|
||||
<text v-else class="placeholder">请选择用户</text>
|
||||
<view class="selector-arrow">▼</view>
|
||||
</view>
|
||||
<view class="room-footer">
|
||||
<text class="last-message">{{ getPlainTextPreview(room.lastMessage) }}</text>
|
||||
<view class="unread-badge" v-if="room.unreadCount && room.unreadCount > 0">
|
||||
<text class="badge-text">{{ room.unreadCount > 99 ? '99+' : room.unreadCount }}</text>
|
||||
<view class="btn-group">
|
||||
<view class="reset-btn" @tap="clearGuestSelect">重置</view>
|
||||
<view class="query-btn" @tap="handleQuery">查询</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天室列表 -->
|
||||
<scroll-view class="list" scroll-y="true"
|
||||
:style="{ marginTop: navTotalHeight + (isGuest ? 0 : 44) + 'px' }"
|
||||
@scrolltolower="handleScrollToLower">
|
||||
<view class="room-card" v-for="(room, index) in chatRooms" :key="index" @tap="enterRoom(room)">
|
||||
<view class="room-avatar">
|
||||
<text class="avatar-text">{{ room.guestName?.charAt(0) || '客' }}</text>
|
||||
</view>
|
||||
<view class="room-info">
|
||||
<view class="room-header">
|
||||
<text class="room-name">{{ room.roomName || '聊天室' }}</text>
|
||||
<text class="room-time">{{ formatTime(room.lastMessageTime) }}</text>
|
||||
</view>
|
||||
<view class="room-footer">
|
||||
<text class="last-message">{{ getPlainTextPreview(room.lastMessage) }}</text>
|
||||
<view class="unread-badge" v-if="room.unreadCount && room.unreadCount > 0">
|
||||
<text class="badge-text">{{ room.unreadCount > 99 ? '99+' : room.unreadCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="room-status" :class="getStatusClass(room.status)">
|
||||
<text class="status-dot"></text>
|
||||
<text class="status-text">{{ getStatusText(room.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="room-status" :class="getStatusClass(room.status)">
|
||||
<text class="status-dot"></text>
|
||||
<text class="status-text">{{ getStatusText(room.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="chatRooms.length === 0">
|
||||
<text class="empty-icon">💬</text>
|
||||
<text class="empty-text">暂无聊天室</text>
|
||||
<text class="empty-hint">点击"联系人工"创建新聊天室</text>
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="chatRooms.length === 0">
|
||||
<text class="empty-icon">💬</text>
|
||||
<text class="empty-text">暂无聊天室</text>
|
||||
<text class="empty-hint">点击"联系人工"创建新聊天室</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view class="load-more" v-if="loading">
|
||||
<text class="load-text">加载中...</text>
|
||||
</view>
|
||||
<view class="load-more" v-else-if="!hasMore && chatRooms.length > 0">
|
||||
<text class="load-text">没有更多数据了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<view class="modal-mask" v-if="showGuestSelector" @tap="showGuestSelector = false"></view>
|
||||
<view class="modal-content" v-if="showGuestSelector">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">选择用户</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="guest-item" v-for="guest in guestsList" :key="guest.userId"
|
||||
@tap="handleGuestSelect(guest)">
|
||||
<text class="guest-name">{{ guest.name }} {{ guest.phone || '无电话' }}</text>
|
||||
</view>
|
||||
<view class="empty-item" v-if="guestsList.length === 0">
|
||||
<text>暂无可选用户</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { workcaseChatAPI } from '@/api'
|
||||
import type { ChatRoomVO, TbChatRoomDTO, PageRequest, ChatRoomMessageVO } from '@/types'
|
||||
import { wsClient } from '@/utils/websocket'
|
||||
import { WS_HOST } from '@/config'
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { workcaseChatAPI } from '@/api'
|
||||
import { guestAPI } from '@/api/sys/guest'
|
||||
import type { ChatRoomVO, TbChatRoomDTO, PageRequest, ChatRoomMessageVO, TbGuestDTO } from '@/types'
|
||||
import { wsClient } from '@/utils/websocket'
|
||||
import { WS_HOST } from '@/config'
|
||||
|
||||
// 导航栏
|
||||
const navPaddingTop = ref<number>(0)
|
||||
const navHeight = ref<number>(44)
|
||||
const capsuleHeight = ref<number>(32)
|
||||
// 导航栏
|
||||
const navPaddingTop = ref<number>(0)
|
||||
const navHeight = ref<number>(44)
|
||||
const navTotalHeight = ref<number>(44)
|
||||
const capsuleHeight = ref<number>(32)
|
||||
|
||||
// 加载状态
|
||||
const loading = ref<boolean>(false)
|
||||
// 加载状态
|
||||
const loading = ref<boolean>(false)
|
||||
const loadingUsers = ref<boolean>(false)
|
||||
|
||||
// 聊天室列表
|
||||
const chatRooms = ref<ChatRoomVO[]>([])
|
||||
// 聊天室列表
|
||||
const chatRooms = ref<ChatRoomVO[]>([])
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
const statusBarHeight = windowInfo.statusBarHeight || 20
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||
navPaddingTop.value = menuButton.top
|
||||
capsuleHeight.value = menuButton.height
|
||||
navHeight.value = menuButton.bottom + 8
|
||||
} catch (e) {
|
||||
// 分页相关
|
||||
const currentPage = ref<number>(1)
|
||||
const pageSize = ref<number>(10)
|
||||
const total = ref<number>(0)
|
||||
const hasMore = ref<boolean>(true)
|
||||
|
||||
// 用户筛选相关
|
||||
const isGuest = ref(true)
|
||||
const guestsList = ref<TbGuestDTO[]>([])
|
||||
const selectedGuestId = ref<string>('')
|
||||
const showGuestSelector = ref<boolean>(false)
|
||||
|
||||
// 计算属性:根据选中的用户ID筛选聊天室
|
||||
const filteredChatRooms = computed(() => {
|
||||
if (!selectedGuestId.value) {
|
||||
return chatRooms.value
|
||||
}
|
||||
return chatRooms.value.filter(room => room.guestId === selectedGuestId.value)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
const statusBarHeight = windowInfo.statusBarHeight || 20
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||
navPaddingTop.value = menuButton.top
|
||||
capsuleHeight.value = menuButton.height
|
||||
navHeight.value = menuButton.bottom + 8
|
||||
} catch (e) {
|
||||
navPaddingTop.value = statusBarHeight
|
||||
navHeight.value = navPaddingTop.value + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
navPaddingTop.value = statusBarHeight
|
||||
navHeight.value = navPaddingTop.value + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
navPaddingTop.value = statusBarHeight
|
||||
navHeight.value = navPaddingTop.value + 44
|
||||
// #endif
|
||||
|
||||
loadChatRooms()
|
||||
initWebSocket()
|
||||
})
|
||||
// #endif
|
||||
|
||||
// 组件卸载时断开WebSocket
|
||||
onUnmounted(() => {
|
||||
disconnectWebSocket()
|
||||
})
|
||||
// 设置导航栏总高度
|
||||
navTotalHeight.value = navHeight.value
|
||||
|
||||
// 加载聊天室列表
|
||||
async function loadChatRooms() {
|
||||
loading.value = true
|
||||
try {
|
||||
// 获取当前用户ID
|
||||
const userId = uni.getStorageSync('userId') || ''
|
||||
|
||||
const pageRequest: PageRequest<TbChatRoomDTO> = {
|
||||
filter: {
|
||||
guestId: userId // 查询当前用户的聊天室
|
||||
},
|
||||
pageParam: {
|
||||
pageNumber: 1,
|
||||
pageSize: 50
|
||||
}
|
||||
}
|
||||
|
||||
const res = await workcaseChatAPI.getChatRoomPage(pageRequest)
|
||||
if (res.success && res.pageDomain?.dataList) {
|
||||
chatRooms.value = res.pageDomain.dataList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载聊天室列表失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 检查用户类型
|
||||
checkUserType()
|
||||
|
||||
// 格式化时间(兼容 iOS)
|
||||
function formatTime(time?: string): string {
|
||||
if (!time) return ''
|
||||
// iOS 不支持 "yyyy-MM-dd HH:mm:ss" 格式,需要转换为 "yyyy-MM-ddTHH:mm:ss"
|
||||
const iosCompatibleTime = time.replace(' ', 'T')
|
||||
const date = new Date(iosCompatibleTime)
|
||||
if (isNaN(date.getTime())) return ''
|
||||
// 加载聊天室列表
|
||||
loadChatRooms()
|
||||
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
|
||||
if (diff < 172800000) return '昨天'
|
||||
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||
}
|
||||
|
||||
// 去除markdown语法并截取前10个字符
|
||||
function getPlainTextPreview(text?: string): string {
|
||||
if (!text) return '暂无消息'
|
||||
|
||||
// 去除markdown语法
|
||||
let plainText = text
|
||||
// 去除代码块
|
||||
.replace(/```[\s\S]*?```/g, '[代码]')
|
||||
// 去除行内代码
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
// 去除粗体
|
||||
.replace(/\*\*([^\*]+)\*\*/g, '$1')
|
||||
// 去除斜体
|
||||
.replace(/\*([^\*]+)\*/g, '$1')
|
||||
// 去除链接
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
// 去除标题标记
|
||||
.replace(/^#{1,6}\s+/gm, '')
|
||||
// 去除列表标记
|
||||
.replace(/^[*-]\s+/gm, '')
|
||||
// 去除多余的空白字符
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
// 截取前10个字符
|
||||
if (plainText.length > 10) {
|
||||
return plainText.substring(0, 10) + '...'
|
||||
}
|
||||
|
||||
return plainText
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
function getStatusClass(status?: string): string {
|
||||
switch (status) {
|
||||
case 'active': return 'status-active'
|
||||
case 'waiting': return 'status-waiting'
|
||||
case 'closed': return 'status-closed'
|
||||
default: return 'status-waiting'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string): string {
|
||||
switch (status) {
|
||||
case 'active': return '进行中'
|
||||
case 'waiting': return '等待中'
|
||||
case 'closed': return '已关闭'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 进入聊天室
|
||||
function enterRoom(room: ChatRoomVO) {
|
||||
room.unreadCount = 0
|
||||
uni.navigateTo({
|
||||
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${room.roomId}&workcaseId=${room.workcaseId || ''}`
|
||||
// 初始化WebSocket
|
||||
initWebSocket()
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
// 组件卸载时断开WebSocket
|
||||
onUnmounted(() => {
|
||||
disconnectWebSocket()
|
||||
})
|
||||
|
||||
// ==================== WebSocket连接管理 ====================
|
||||
|
||||
// 初始化WebSocket连接
|
||||
async function initWebSocket() {
|
||||
try {
|
||||
const token = uni.getStorageSync('token') || ''
|
||||
if (!token) {
|
||||
console.warn('[chatRoomList] 未找到token,跳过WebSocket连接')
|
||||
return
|
||||
}
|
||||
|
||||
// 构建WebSocket URL
|
||||
// 开发环境:ws://localhost:8180 或 ws://192.168.x.x:8180
|
||||
// 生产环境:wss://your-domain.com
|
||||
const protocol = 'wss:' // 开发环境使用ws,生产环境改为wss
|
||||
// 小程序使用原生WebSocket端点(不是SockJS端点)
|
||||
const wsUrl = `${protocol}//${WS_HOST}/urban-lifeline/workcase/ws/chat?token=${encodeURIComponent(token)}`
|
||||
|
||||
console.log('[chatRoomList] 开始连接WebSocket')
|
||||
await wsClient.connect(wsUrl, token)
|
||||
|
||||
// 订阅聊天室列表更新频道
|
||||
wsClient.subscribe('/topic/chat/list-update', handleListUpdate)
|
||||
console.log('[chatRoomList] WebSocket连接成功,已订阅列表更新频道')
|
||||
} catch (error) {
|
||||
console.error('[chatRoomList] WebSocket连接失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 断开WebSocket连接
|
||||
function disconnectWebSocket() {
|
||||
try {
|
||||
wsClient.disconnect()
|
||||
console.log('[chatRoomList] WebSocket已断开')
|
||||
} catch (error) {
|
||||
console.error('[chatRoomList] 断开WebSocket失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理列表更新消息
|
||||
async function handleListUpdate(message: ChatRoomMessageVO) {
|
||||
console.log('[chatRoomList] 收到列表更新消息:', message)
|
||||
|
||||
// 更新对应聊天室的lastMessage和lastMessageTime
|
||||
const roomIndex = chatRooms.value.findIndex((r: ChatRoomVO) => r.roomId === message.roomId)
|
||||
if (roomIndex !== -1) {
|
||||
// 查询当前用户在该聊天室的未读数
|
||||
let unreadCount = 0
|
||||
// 检查用户类型
|
||||
function checkUserType() {
|
||||
try {
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
const userId = typeof userInfo === 'string' ? JSON.parse(userInfo).userId : userInfo?.userId
|
||||
if (userId) {
|
||||
const unreadResult = await workcaseChatAPI.getUnreadCount(message.roomId, userId)
|
||||
if (unreadResult.success && unreadResult.data !== undefined) {
|
||||
unreadCount = unreadResult.data
|
||||
}
|
||||
const parsedUserInfo = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
|
||||
isGuest.value = parsedUserInfo.status === 'guest'
|
||||
|
||||
// 如果是非guest用户,加载可选人员列表
|
||||
if (!isGuest.value) {
|
||||
loadGuestsList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[chatRoomList] 查询未读数失败:', error)
|
||||
console.error('检查用户类型失败:', error)
|
||||
isGuest.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 加载可选人员列表
|
||||
async function loadGuestsList() {
|
||||
loadingUsers.value = true
|
||||
try {
|
||||
const res = await guestAPI.listGuest()
|
||||
if (res.success && res.dataList) {
|
||||
guestsList.value = res.dataList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载可选人员列表失败:', error)
|
||||
} finally {
|
||||
loadingUsers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载聊天室列表
|
||||
async function loadChatRooms(isLoadMore = false) {
|
||||
// 如果正在加载或没有更多数据,直接返回
|
||||
if (loading.value || (!isLoadMore && !hasMore.value)) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 获取当前用户ID
|
||||
const userId = uni.getStorageSync('userId') || ''
|
||||
|
||||
// 计算当前页码
|
||||
const currentPageNum = isLoadMore ? currentPage.value + 1 : 1
|
||||
|
||||
const pageRequest : PageRequest<TbChatRoomDTO> = {
|
||||
filter: {
|
||||
guestId: isGuest.value ? userId : selectedGuestId.value // 如果是guest用户,查询自己的聊天室;否则根据选中的用户ID查询
|
||||
},
|
||||
pageParam: {
|
||||
page: currentPageNum,
|
||||
pageSize: pageSize.value
|
||||
}
|
||||
}
|
||||
|
||||
const res = await workcaseChatAPI.getChatRoomPage(pageRequest)
|
||||
if (res.success && res.pageDomain?.dataList) {
|
||||
const newRooms = res.pageDomain.dataList
|
||||
total.value = res.pageDomain?.pageParam?.total || 0
|
||||
|
||||
// 根据是否加载更多来处理数据
|
||||
if (isLoadMore) {
|
||||
// 加载更多:追加数据
|
||||
chatRooms.value = [...chatRooms.value, ...newRooms]
|
||||
currentPage.value++
|
||||
} else {
|
||||
// 刷新:替换数据
|
||||
chatRooms.value = newRooms
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
hasMore.value = chatRooms.value.length < total.value
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载聊天室列表失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
// 如果是加载更多失败,保持hasMore不变
|
||||
if (!isLoadMore) {
|
||||
hasMore.value = false
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理滚动到底部事件
|
||||
function handleScrollToLower() {
|
||||
if (hasMore.value && !loading.value) {
|
||||
loadChatRooms(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理用户选择
|
||||
function handleGuestSelect(guest : TbGuestDTO) {
|
||||
selectedGuestId.value = guest.userId || ''
|
||||
showGuestSelector.value = false
|
||||
// 重置分页状态
|
||||
hasMore.value = true
|
||||
loadChatRooms() // 根据选中的用户重新加载聊天室列表
|
||||
}
|
||||
|
||||
// 清除用户选择
|
||||
function clearGuestSelect() {
|
||||
selectedGuestId.value = ''
|
||||
// 重置分页状态
|
||||
hasMore.value = true
|
||||
loadChatRooms() // 重新加载所有聊天室列表
|
||||
}
|
||||
|
||||
// 查询聊天室列表
|
||||
function handleQuery() {
|
||||
// 重置分页状态
|
||||
hasMore.value = true
|
||||
loadChatRooms() // 根据当前筛选条件重新加载聊天室列表
|
||||
}
|
||||
|
||||
// 格式化时间(兼容 iOS)
|
||||
function formatTime(time ?: string) : string {
|
||||
if (!time) return ''
|
||||
// iOS 不支持 "yyyy-MM-dd HH:mm:ss" 格式,需要转换为 "yyyy-MM-ddTHH:mm:ss"
|
||||
const iosCompatibleTime = time.replace(' ', 'T')
|
||||
const date = new Date(iosCompatibleTime)
|
||||
if (isNaN(date.getTime())) return ''
|
||||
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
|
||||
if (diff < 172800000) return '昨天'
|
||||
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||
}
|
||||
|
||||
// 去除markdown语法并截取前10个字符
|
||||
function getPlainTextPreview(text ?: string) : string {
|
||||
if (!text) return '暂无消息'
|
||||
|
||||
// 去除markdown语法
|
||||
let plainText = text
|
||||
// 去除代码块
|
||||
.replace(/```[\s\S]*?```/g, '[代码]')
|
||||
// 去除行内代码
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
// 去除粗体
|
||||
.replace(/\*\*([^\*]+)\*\*/g, '$1')
|
||||
// 去除斜体
|
||||
.replace(/\*([^\*]+)\*/g, '$1')
|
||||
// 去除链接
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
// 去除标题标记
|
||||
.replace(/^#{1,6}\s+/gm, '')
|
||||
// 去除列表标记
|
||||
.replace(/^[*-]\s+/gm, '')
|
||||
// 去除多余的空白字符
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
// 截取前10个字符
|
||||
if (plainText.length > 10) {
|
||||
return plainText.substring(0, 10) + '...'
|
||||
}
|
||||
|
||||
return plainText
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
function getStatusClass(status ?: string) : string {
|
||||
switch (status) {
|
||||
case 'active': return 'status-active'
|
||||
case 'waiting': return 'status-waiting'
|
||||
case 'closed': return 'status-closed'
|
||||
default: return 'status-waiting'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status ?: string) : string {
|
||||
switch (status) {
|
||||
case 'active': return '进行中'
|
||||
case 'waiting': return '等待中'
|
||||
case 'closed': return '已关闭'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 进入聊天室
|
||||
function enterRoom(room : ChatRoomVO) {
|
||||
room.unreadCount = 0
|
||||
uni.navigateTo({
|
||||
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${room.roomId}&workcaseId=${room.workcaseId || ''}`
|
||||
})
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// ==================== WebSocket连接管理 ====================
|
||||
|
||||
// 初始化WebSocket连接
|
||||
async function initWebSocket() {
|
||||
try {
|
||||
const token = uni.getStorageSync('token') || ''
|
||||
if (!token) {
|
||||
console.warn('[chatRoomList] 未找到token,跳过WebSocket连接')
|
||||
return
|
||||
}
|
||||
|
||||
// 构建WebSocket URL
|
||||
// 开发环境:ws://localhost:8180 或 ws://192.168.x.x:8180
|
||||
// 生产环境:wss://your-domain.com
|
||||
const protocol = 'wss:' // 开发环境使用ws,生产环境改为wss
|
||||
// 小程序使用原生WebSocket端点(不是SockJS端点)
|
||||
const wsUrl = `${protocol}//${WS_HOST}/urban-lifeline/workcase/ws/chat?token=${encodeURIComponent(token)}`
|
||||
|
||||
console.log('[chatRoomList] 开始连接WebSocket')
|
||||
await wsClient.connect(wsUrl, token)
|
||||
|
||||
// 订阅聊天室列表更新频道
|
||||
wsClient.subscribe('/topic/chat/list-update', handleListUpdate)
|
||||
console.log('[chatRoomList] WebSocket连接成功,已订阅列表更新频道')
|
||||
} catch (error) {
|
||||
console.error('[chatRoomList] WebSocket连接失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 断开WebSocket连接
|
||||
function disconnectWebSocket() {
|
||||
try {
|
||||
wsClient.disconnect()
|
||||
console.log('[chatRoomList] WebSocket已断开')
|
||||
} catch (error) {
|
||||
console.error('[chatRoomList] 断开WebSocket失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理列表更新消息
|
||||
async function handleListUpdate(message : ChatRoomMessageVO) {
|
||||
console.log('[chatRoomList] 收到列表更新消息:', message)
|
||||
|
||||
// 更新对应聊天室的lastMessage和lastMessageTime
|
||||
const roomIndex = chatRooms.value.findIndex((r : ChatRoomVO) => r.roomId === message.roomId)
|
||||
if (roomIndex !== -1) {
|
||||
// 查询当前用户在该聊天室的未读数
|
||||
let unreadCount = 0
|
||||
try {
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
const userId = typeof userInfo === 'string' ? JSON.parse(userInfo).userId : userInfo?.userId
|
||||
if (userId) {
|
||||
const unreadResult = await workcaseChatAPI.getUnreadCount(message.roomId, userId)
|
||||
if (unreadResult.success && unreadResult.data !== undefined) {
|
||||
unreadCount = unreadResult.data
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[chatRoomList] 查询未读数失败:', error)
|
||||
}
|
||||
|
||||
chatRooms.value[roomIndex] = {
|
||||
...chatRooms.value[roomIndex],
|
||||
lastMessage: message.content || '',
|
||||
lastMessageTime: message.sendTime || '',
|
||||
unreadCount: unreadCount
|
||||
}
|
||||
|
||||
// 将更新的聊天室移到列表顶部
|
||||
const updatedRoom = chatRooms.value[roomIndex]
|
||||
chatRooms.value.splice(roomIndex, 1)
|
||||
chatRooms.value.unshift(updatedRoom)
|
||||
}
|
||||
|
||||
chatRooms.value[roomIndex] = {
|
||||
...chatRooms.value[roomIndex],
|
||||
lastMessage: message.content || '',
|
||||
lastMessageTime: message.sendTime || '',
|
||||
unreadCount: unreadCount
|
||||
}
|
||||
|
||||
// 将更新的聊天室移到列表顶部
|
||||
const updatedRoom = chatRooms.value[roomIndex]
|
||||
chatRooms.value.splice(roomIndex, 1)
|
||||
chatRooms.value.unshift(updatedRoom)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./chatRoomList.scss";
|
||||
@import "./chatRoomList.scss";
|
||||
|
||||
// 用户筛选组件样式
|
||||
.filter-section {
|
||||
position: fixed;
|
||||
top: var(--nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 44px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.guest-selector {
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 16px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f9f9f9;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.guest-selector:active {
|
||||
background: #e9e9e9;
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
.selected-guest {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.selector-arrow {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-section:active .selector-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 按钮组样式 */
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.reset-btn,
|
||||
.query-btn {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.reset-btn:active {
|
||||
background: #e9e9e9;
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
background: #1989fa;
|
||||
border: 1px solid #1989fa;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.query-btn:active {
|
||||
background: #0066cc;
|
||||
border-color: #0066cc;
|
||||
}
|
||||
|
||||
// 弹窗样式
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.25);
|
||||
z-index: 1001;
|
||||
animation: slideUp 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 弹窗动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -45%);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.modal-close:active {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.modal-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
.guest-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 16px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.guest-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.guest-item:active {
|
||||
background-color: #f8f8f8;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.guest-item:active .guest-name {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.guest-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.guest-phone {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.empty-item {
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.empty-item::before {
|
||||
content: "👤";
|
||||
font-size: 48px;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 加载更多样式 */
|
||||
.load-more {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.load-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,8 @@
|
||||
position: relative;
|
||||
// 多种安全区域适配方式
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-top: constant(safe-area-inset-top); /* 兼容iOS < 11.2 */
|
||||
padding-top: constant(safe-area-inset-top);
|
||||
/* 兼容iOS < 11.2 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -18,12 +19,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
z-index: 100;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
// 小程序需要为右侧胶囊按钮留出空间
|
||||
/* #ifdef MP-WEIXIN */
|
||||
padding-right: 100px;
|
||||
@@ -83,7 +84,7 @@
|
||||
// 退出按钮特殊样式
|
||||
.logout-btn {
|
||||
background: rgba(255, 59, 48, 0.1);
|
||||
|
||||
|
||||
.btn-text {
|
||||
color: #ff3b30;
|
||||
}
|
||||
@@ -117,10 +118,25 @@
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.r1 { width: 260px; height: 260px; }
|
||||
.r2 { width: 200px; height: 200px; }
|
||||
.r3 { width: 150px; height: 150px; }
|
||||
.r4 { width: 110px; height: 110px; }
|
||||
.r1 {
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.r2 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.r3 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.r4 {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.robot {
|
||||
position: relative;
|
||||
@@ -128,10 +144,10 @@
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
/* 父容器加一层径向渐变背景,模拟外层模糊光晕 */
|
||||
background: radial-gradient(circle at center,
|
||||
rgba(180, 220, 255, 0.5) 0%,
|
||||
rgba(180, 220, 255, 0.25) 50%,
|
||||
transparent 75%);
|
||||
background: radial-gradient(circle at center,
|
||||
rgba(180, 220, 255, 0.5) 0%,
|
||||
rgba(180, 220, 255, 0.25) 50%,
|
||||
transparent 75%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -164,16 +180,30 @@
|
||||
.float-tag {
|
||||
position: absolute;
|
||||
padding: 6px 12px;
|
||||
background: transparent; /* 去掉背景色,和左侧一致 */
|
||||
border: none; /* 去掉边框 */
|
||||
border-radius: 0; /* 保持直角(如果需要圆角也可以改回16px) */
|
||||
background: transparent;
|
||||
/* 去掉背景色,和左侧一致 */
|
||||
border: none;
|
||||
/* 去掉边框 */
|
||||
border-radius: 0;
|
||||
/* 保持直角(如果需要圆角也可以改回16px) */
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.t1 { right: 20px; top: 40px; }
|
||||
.t2 { left: 20px; top: 80px; }
|
||||
.t3 { right: 30px; bottom: 50px; }
|
||||
.t1 {
|
||||
right: 20px;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.t2 {
|
||||
left: 20px;
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.t3 {
|
||||
right: 30px;
|
||||
bottom: 50px;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
text-align: left;
|
||||
@@ -199,7 +229,7 @@
|
||||
background: #fff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
margin: 16px;
|
||||
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
@@ -373,63 +403,86 @@
|
||||
padding-left: 48px;
|
||||
}
|
||||
|
||||
// 底部操作区域
|
||||
// 底部操作区域(如果没有定义,需要添加)
|
||||
.bottom-area {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
// background: rgba(240, 241, 246, 0.95);
|
||||
padding: 12px 16px;
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 12px 16px;
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
z-index: 50;
|
||||
background-color: #fff; // 增加背景色,避免穿透
|
||||
}
|
||||
|
||||
// 快捷按钮横向滚动
|
||||
// 快捷按钮横向滚动 - 修复横向滚动问题
|
||||
.quick-scroll {
|
||||
white-space: nowrap;
|
||||
margin-bottom: 12px;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
/* 核心:必须设置固定高度,否则scroll-view可能无法正常工作 */
|
||||
height: 50px;
|
||||
/* 确保scroll-view能正确处理滚动 */
|
||||
overflow-x: scroll;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.quick-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center; /* ✅ 垂直居中 */
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
}
|
||||
/* 隐藏滚动条 */
|
||||
.quick-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.quick-list {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
display: inline-flex; /* 使用inline-flex确保宽度随内容增长,触发横向滚动 */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
/* 关键:让容器宽度自动适应内容,超出父容器时触发滚动 */
|
||||
width: auto;
|
||||
/* 确保垂直居中 */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
flex-basis: auto;
|
||||
align-content: stretch;
|
||||
min-height: 0px;
|
||||
min-width: 0px;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px 24px;
|
||||
color: black;
|
||||
/* 保持原有样式 */
|
||||
margin: 0;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border: none;
|
||||
padding: 10px 24px;
|
||||
color: black;
|
||||
/* 确保按钮不换行 */
|
||||
white-space: nowrap;
|
||||
/* 关键:防止flex子项被压缩,确保产生横向溢出以便滚动 */
|
||||
flex-shrink: 0;
|
||||
/* 按钮内部使用flex布局 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
/* 确保按钮高度合适 */
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.quick-btn.has-icon {
|
||||
// background: linear-gradient(90deg, #173294 0%, #4a6fd9 100%);
|
||||
border: none;
|
||||
padding: 10px 24px;
|
||||
/* 保持原有样式 */
|
||||
}
|
||||
|
||||
.quick-btn.has-icon .quick-text {
|
||||
// color: #fff;
|
||||
font-weight: 500;
|
||||
/* 保持原有样式 */
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
@@ -443,6 +496,7 @@
|
||||
height: 20px;
|
||||
background: #d0d5dd;
|
||||
margin: 0 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-text {
|
||||
@@ -696,10 +750,14 @@
|
||||
}
|
||||
|
||||
@keyframes typing-bounce {
|
||||
0%, 80%, 100% {
|
||||
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
@@ -953,3 +1011,77 @@
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// 操作选择弹窗样式
|
||||
.operation-select-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.operation-select-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.operation-select-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.operation-select-item:active {
|
||||
background: #e4e7ed;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.select-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.select-text {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
@@ -7,13 +7,13 @@
|
||||
<button class="workcase-btn" @tap="showUserSelector">
|
||||
<text class="btn-text">{{userInfo.username || '切换'}}</text>
|
||||
</button>
|
||||
<button class="workcase-btn" @tap="goToChatRoomList">
|
||||
<!-- <button class="workcase-btn" @tap="showOperationSelect('chat')">
|
||||
<text class="btn-text">聊天室</text>
|
||||
</button>
|
||||
<button class="workcase-btn" @tap="goToWorkList">
|
||||
<button class="workcase-btn" @tap="showOperationSelect('workcase')">
|
||||
<image class="btn-icon" src="/static/imgs/case.svg" />
|
||||
<text class="btn-text">工单</text>
|
||||
</button>
|
||||
</button> -->
|
||||
</view>
|
||||
</view>
|
||||
<!-- 欢迎区域(机器人+浮动标签) -->
|
||||
@@ -41,7 +41,8 @@
|
||||
</view>
|
||||
|
||||
<!-- 聊天消息区域 -->
|
||||
<scroll-view class="chat-messages" scroll-y="true" :scroll-top="scrollTop" scroll-with-animation="true" :class="{ started: messages.length > 0 }">
|
||||
<scroll-view class="chat-messages" scroll-y="true" :scroll-top="scrollTop" scroll-with-animation="true"
|
||||
:class="{ started: messages.length > 0 }">
|
||||
<!-- AI初始消息 -->
|
||||
<view class="ai-initial-msg" v-if="messages.length === 0">
|
||||
<text class="ai-msg-text">您好,我是泰豪小电智能客服。请描述您的问题,我会尽力协助。</text>
|
||||
@@ -59,7 +60,8 @@
|
||||
</view>
|
||||
<!-- 用户消息的文件列表(在气泡外面) -->
|
||||
<view v-if="item.files && item.files.length > 0" class="message-files">
|
||||
<view v-for="fileId in item.files" :key="fileId" class="message-file-item" @tap="previewFile(fileId)">
|
||||
<view v-for="fileId in item.files" :key="fileId" class="message-file-item"
|
||||
@tap="previewFile(fileId)">
|
||||
<view v-if="isImageFileById(fileId)" class="file-thumb image">
|
||||
<image :src="getFileDownloadUrl(fileId)" mode="aspectFill" class="file-img" />
|
||||
</view>
|
||||
@@ -86,69 +88,74 @@
|
||||
<view class="typing-dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
</view>
|
||||
<rich-text v-else :nodes="renderMarkdown(item.content)" class="message-rich-text"></rich-text>
|
||||
<rich-text v-else :nodes="renderMarkdown(item.content)"
|
||||
class="message-rich-text"></rich-text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="message-time">{{item.time}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<!-- 底部操作区域 -->
|
||||
<view class="bottom-area">
|
||||
<!-- 快捷按钮横向滚动 -->
|
||||
<scroll-view class="quick-scroll" scroll-x="true">
|
||||
<view class="quick-list">
|
||||
<view class="quick-btn has-icon" @tap="contactHuman">
|
||||
<text class="quick-icon">☎</text>
|
||||
<text class="quick-text">联系人工</text>
|
||||
</view>
|
||||
<view class="quick-btn has-icon" @tap="showCreator">
|
||||
<text class="quick-icon">⊕</text>
|
||||
<text class="quick-text">创建工单</text>
|
||||
</view>
|
||||
<view class="quick-divider"></view>
|
||||
<view class="quick-btn" @tap="handleQuickQuestion('查询质保状态')">
|
||||
<text class="quick-text">查询质保状态</text>
|
||||
</view>
|
||||
<view class="quick-btn" @tap="handleQuickQuestion('发动机无法启动')">
|
||||
<text class="quick-text">发动机无法启动</text>
|
||||
</view>
|
||||
<!-- 快捷按钮横向滚动 -->
|
||||
<scroll-view class="quick-scroll" scroll-x="true" show-scrollbar="false">
|
||||
<view class="quick-inner">
|
||||
<view class="quick-btn has-icon" @tap="showOperationSelect('chat')">
|
||||
<text class="quick-icon">☎</text>
|
||||
<text class="quick-text">联系人工</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="quick-btn has-icon" @tap="showOperationSelect('workcase')">
|
||||
<text class="quick-icon">⊕</text>
|
||||
<text class="quick-text">创建工单</text>
|
||||
</view>
|
||||
<view class="quick-divider"></view>
|
||||
<view class="quick-btn" @tap="handleQuickQuestion('查询质保状态')">
|
||||
<text class="quick-text">查询质保状态</text>
|
||||
</view>
|
||||
<view class="quick-btn" @tap="handleQuickQuestion('发动机无法启动')">
|
||||
<text class="quick-text">发动机无法启动</text>
|
||||
</view>
|
||||
<view class="quick-btn" @tap="handleQuickQuestion('申请上门维修')">
|
||||
<text class="quick-text">申请上门维修服务</text>
|
||||
</view>
|
||||
<view class="quick-btn" @tap="handleQuickQuestion('查询维修进度')">
|
||||
<text class="quick-text">查询维修进度</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="chat-input-wrap">
|
||||
<!-- 已上传文件预览 -->
|
||||
<view v-if="uploadedFiles.length > 0" class="uploaded-files">
|
||||
<view v-for="(file, index) in uploadedFiles" :key="file.id || index" class="uploaded-file-item">
|
||||
<view v-if="isImageFile(file)" class="file-preview-image">
|
||||
<image :src="getFilePreviewUrl(file)" mode="aspectFill" class="preview-img" />
|
||||
</view>
|
||||
<view v-else class="file-preview-doc">
|
||||
<text class="doc-icon">📄</text>
|
||||
</view>
|
||||
<text class="file-name">{{file.name || '文件'}}</text>
|
||||
<view class="remove-file-btn" @tap="removeUploadedFile(index)">
|
||||
<text class="remove-icon">✕</text>
|
||||
</view>
|
||||
<!-- 输入区域 -->
|
||||
<view class="chat-input-wrap">
|
||||
<!-- 已上传文件预览 -->
|
||||
<view v-if="uploadedFiles.length > 0" class="uploaded-files">
|
||||
<view v-for="(file, index) in uploadedFiles" :key="file.id || index" class="uploaded-file-item">
|
||||
<view v-if="isImageFile(file)" class="file-preview-image">
|
||||
<image :src="getFilePreviewUrl(file)" mode="aspectFill" class="preview-img" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 输入框 -->
|
||||
<view class="input-row">
|
||||
<view class="upload-btn" :class="{ uploading: isUploading }" @tap="showUploadOptions">
|
||||
<text v-if="isUploading" class="upload-icon">⏳</text>
|
||||
<text v-else class="upload-icon">📎</text>
|
||||
<view v-else class="file-preview-doc">
|
||||
<text class="doc-icon">📄</text>
|
||||
</view>
|
||||
<input class="chat-input" v-model="inputText" placeholder="输入问题 来问问我~" @confirm="sendMessage" />
|
||||
<view class="send-btn" :class="{ active: inputText.trim() || uploadedFiles.length > 0 }" @tap="sendMessage">
|
||||
<text class="send-icon">➤</text>
|
||||
<text class="file-name">{{file.name || '文件'}}</text>
|
||||
<view class="remove-file-btn" @tap="removeUploadedFile(index)">
|
||||
<text class="remove-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 输入框 -->
|
||||
<view class="input-row">
|
||||
<view class="upload-btn" :class="{ uploading: isUploading }" @tap="showUploadOptions">
|
||||
<text v-if="isUploading" class="upload-icon">⏳</text>
|
||||
<text v-else class="upload-icon">📎</text>
|
||||
</view>
|
||||
<input class="chat-input" v-model="inputText" placeholder="输入问题 来问问我~" @confirm="sendMessage" />
|
||||
<view class="send-btn" :class="{ active: inputText.trim() || uploadedFiles.length > 0 }"
|
||||
@tap="sendMessage">
|
||||
<text class="send-icon">➤</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 设备代码输入弹窗 -->
|
||||
<view class="device-code-modal" v-if="showDeviceCodeDialog">
|
||||
<view class="modal-mask" @tap="cancelDeviceCodeInput"></view>
|
||||
@@ -157,13 +164,8 @@
|
||||
<text class="modal-title">请输入设备代码</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<input
|
||||
class="device-code-input"
|
||||
v-model="deviceCodeInput"
|
||||
placeholder="请输入设备代码"
|
||||
focus
|
||||
@confirm="confirmDeviceCodeInput"
|
||||
/>
|
||||
<input class="device-code-input" v-model="deviceCodeInput" placeholder="请输入设备代码" focus
|
||||
@confirm="confirmDeviceCodeInput" />
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="modal-btn cancel" @tap="cancelDeviceCodeInput">
|
||||
@@ -176,6 +178,28 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作选择弹窗 -->
|
||||
<view class="operation-select-modal" v-if="showOperationSelectDialog">
|
||||
<view class="modal-mask" @tap="cancelOperationSelect"></view>
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ operationSelectTitle }}</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="operation-select-list">
|
||||
<view class="operation-select-item" @tap="handleOperationSelect('existing')">
|
||||
<text class="select-icon">📋</text>
|
||||
<text class="select-text">进入已有{{ operationSelectType === 'chat' ? '聊天室' : '工单' }}</text>
|
||||
</view>
|
||||
<view class="operation-select-item" @tap="handleOperationSelect('new')">
|
||||
<text class="select-icon">➕</text>
|
||||
<text class="select-text">创建新{{ operationSelectType === 'chat' ? '聊天室' : '工单' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号授权弹窗 -->
|
||||
<view class="phone-auth-modal" v-if="showPhoneAuthModal">
|
||||
<view class="modal-mask"></view>
|
||||
@@ -237,11 +261,11 @@
|
||||
import { AGENT_ID } from '@/config'
|
||||
// 前端消息展示类型
|
||||
interface ChatMessageItem {
|
||||
type: 'user' | 'bot'
|
||||
content: string
|
||||
time: string
|
||||
actions?: string[] | null
|
||||
files?: string[] // 文件ID数组
|
||||
type : 'user' | 'bot'
|
||||
content : string
|
||||
time : string
|
||||
actions ?: string[] | null
|
||||
files ?: string[] // 文件ID数组
|
||||
}
|
||||
const agentId = AGENT_ID
|
||||
// 响应式数据
|
||||
@@ -268,7 +292,7 @@
|
||||
userId: ''
|
||||
})
|
||||
const userType = ref(false)
|
||||
|
||||
|
||||
// 是否显示手机号授权弹窗
|
||||
const showPhoneAuthModal = ref(false)
|
||||
// 临时保存的微信登录code
|
||||
@@ -283,6 +307,12 @@
|
||||
const deviceCodeInput = ref<string>('') // 弹窗中的设备代码输入
|
||||
const pendingAction = ref<'workcase' | 'human' | ''>('') // 待执行的操作类型
|
||||
|
||||
// 操作选择弹窗相关
|
||||
const showOperationSelectDialog = ref<boolean>(false) // 是否显示操作选择弹窗
|
||||
const operationSelectType = ref<'chat' | 'workcase'>('chat') // 选择类型:chat=聊天室, workcase=工单
|
||||
const operationSelectTitle = ref<string>('') // 弹窗标题
|
||||
const pendingOperation = ref<'existing' | 'new' | ''>('') // 待执行的操作:existing=进入已有, new=创建新的
|
||||
|
||||
// 初始化用户信息
|
||||
async function initUserInfo() {
|
||||
try {
|
||||
@@ -290,7 +320,7 @@
|
||||
const cachedWechatId = uni.getStorageSync('wechatId')
|
||||
const cachedUserInfo = uni.getStorageSync('userInfo')
|
||||
const cachedToken = uni.getStorageSync('token')
|
||||
|
||||
|
||||
if (cachedWechatId && cachedUserInfo && cachedToken) {
|
||||
// 有完整缓存,直接使用
|
||||
const parsedUserInfo = typeof cachedUserInfo === 'string' ? JSON.parse(cachedUserInfo) : cachedUserInfo
|
||||
@@ -300,14 +330,14 @@
|
||||
phone: parsedUserInfo.phone || '',
|
||||
userId: parsedUserInfo.userId || ''
|
||||
}
|
||||
|
||||
|
||||
// 判断用户类型
|
||||
if (parsedUserInfo.status === 'guest') {
|
||||
userType.value = false
|
||||
} else {
|
||||
userType.value = true
|
||||
}
|
||||
|
||||
|
||||
console.log('使用缓存的用户信息:', userInfo.value)
|
||||
return
|
||||
}
|
||||
@@ -324,7 +354,7 @@
|
||||
// 自动登录
|
||||
async function autoLogin() {
|
||||
uni.showLoading({ title: '初始化中...' })
|
||||
|
||||
|
||||
try {
|
||||
// 使用 wx.login 获取 code
|
||||
uni.login({
|
||||
@@ -332,10 +362,10 @@
|
||||
success: async (loginRes) => {
|
||||
console.log('微信登录成功,code:', loginRes.code)
|
||||
uni.hideLoading()
|
||||
|
||||
|
||||
// 保存code,等待手机号授权后使用
|
||||
tempWechatCode.value = loginRes.code
|
||||
|
||||
|
||||
// 显示手机号授权弹窗
|
||||
showPhoneAuthModal.value = true
|
||||
},
|
||||
@@ -345,7 +375,7 @@
|
||||
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
} catch (error : any) {
|
||||
console.error('自动登录失败:', error)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
@@ -362,7 +392,7 @@
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('loginDomain')
|
||||
uni.removeStorageSync('wechatId')
|
||||
|
||||
|
||||
uni.showLoading({ title: '登录中...' })
|
||||
try {
|
||||
const res = await guestAPI.identify({
|
||||
@@ -379,7 +409,7 @@
|
||||
userInfo.value.userId = loginDomain.user?.userId || ''
|
||||
console.log('identify成功:', loginDomain)
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
if(loginDomain.user.status == 'guest') {
|
||||
if (loginDomain.user.status == 'guest') {
|
||||
userType.value = false
|
||||
} else {
|
||||
userType.value = true
|
||||
@@ -394,12 +424,12 @@
|
||||
}
|
||||
|
||||
// 获取手机号回调(保留用于正式环境)
|
||||
async function onGetPhoneNumber(e: any) {
|
||||
async function onGetPhoneNumber(e : any) {
|
||||
console.log('获取手机号回调:', e)
|
||||
|
||||
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
console.error('获取手机号失败:', e.detail)
|
||||
|
||||
|
||||
// 如果是权限问题,提示用户
|
||||
if (e.detail.errno === 102) {
|
||||
uni.showModal({
|
||||
@@ -418,20 +448,20 @@
|
||||
|
||||
// 关闭弹窗
|
||||
showPhoneAuthModal.value = false
|
||||
|
||||
|
||||
uni.showLoading({ title: '登录中...' })
|
||||
|
||||
try {
|
||||
const code = tempWechatCode.value
|
||||
const wechatId = code.substring(0, 20) // 使用部分code作为临时ID
|
||||
|
||||
|
||||
// 获取手机号授权返回的数据
|
||||
const phoneCode = e.detail.code // 手机号授权code(推荐使用)
|
||||
const encryptedData = e.detail.encryptedData // 加密数据
|
||||
const iv = e.detail.iv // 解密向量
|
||||
|
||||
|
||||
console.log('手机号授权数据:', { code, phoneCode, encryptedData: encryptedData?.substring(0, 50) + '...', iv })
|
||||
|
||||
|
||||
// 调用 identify 接口
|
||||
// 后端可以选择使用 phoneCode 或 encryptedData+iv 来解密手机号
|
||||
const identifyRes = await guestAPI.identify({
|
||||
@@ -442,18 +472,18 @@
|
||||
iv: iv, // 解密向量(旧版API)
|
||||
loginType: 'wechat_miniprogram'
|
||||
})
|
||||
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
|
||||
if (identifyRes.success && identifyRes.data) {
|
||||
const loginDomain = identifyRes.data
|
||||
|
||||
|
||||
// 保存登录信息
|
||||
uni.setStorageSync('token', loginDomain.token || '')
|
||||
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
|
||||
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
|
||||
uni.setStorageSync('wechatId', wechatId)
|
||||
|
||||
|
||||
// 更新用户信息
|
||||
userInfo.value = {
|
||||
wechatId: wechatId,
|
||||
@@ -461,14 +491,14 @@
|
||||
phone: loginDomain.user?.phone || '',
|
||||
userId: loginDomain.user?.userId || ''
|
||||
}
|
||||
|
||||
|
||||
// 判断用户类型
|
||||
if (loginDomain.user?.status === 'guest') {
|
||||
userType.value = false
|
||||
} else {
|
||||
userType.value = true
|
||||
}
|
||||
|
||||
|
||||
console.log('手机号授权登录成功:', userInfo.value)
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
} else {
|
||||
@@ -479,7 +509,7 @@
|
||||
// 登录失败,重新显示授权弹窗
|
||||
showPhoneAuthModal.value = true
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error : any) {
|
||||
console.error('手机号授权登录失败:', error)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
@@ -492,7 +522,7 @@
|
||||
}
|
||||
|
||||
// 选择模拟用户(测试用)
|
||||
async function selectMockUser(phone: string, name: string, wechatId: string) {
|
||||
async function selectMockUser(phone : string, name : string, wechatId : string) {
|
||||
showPhoneAuthModal.value = false
|
||||
uni.showLoading({ title: '登录中...' })
|
||||
|
||||
@@ -505,18 +535,18 @@
|
||||
mockMode: true,
|
||||
loginType: 'wechat_miniprogram'
|
||||
})
|
||||
|
||||
|
||||
uni.hideLoading()
|
||||
|
||||
|
||||
if (identifyRes.success && identifyRes.data) {
|
||||
const loginDomain = identifyRes.data
|
||||
|
||||
|
||||
// 保存登录信息
|
||||
uni.setStorageSync('token', loginDomain.token || '')
|
||||
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
|
||||
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
|
||||
uni.setStorageSync('wechatId', wechatId)
|
||||
|
||||
|
||||
// 更新用户信息
|
||||
userInfo.value = {
|
||||
wechatId: wechatId,
|
||||
@@ -524,14 +554,14 @@
|
||||
phone: phone,
|
||||
userId: loginDomain.user?.userId || ''
|
||||
}
|
||||
|
||||
|
||||
// 判断用户类型
|
||||
if (loginDomain.user?.status === 'guest') {
|
||||
userType.value = false
|
||||
} else {
|
||||
userType.value = true
|
||||
}
|
||||
|
||||
|
||||
console.log('模拟登录成功:', userInfo.value)
|
||||
uni.showToast({ title: `${name} 登录成功`, icon: 'success' })
|
||||
} else {
|
||||
@@ -541,7 +571,7 @@
|
||||
})
|
||||
showPhoneAuthModal.value = true
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error : any) {
|
||||
console.error('模拟登录失败:', error)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
@@ -570,7 +600,7 @@
|
||||
// 获取窗口信息
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
statusBarHeight.value = windowInfo.statusBarHeight || 0
|
||||
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 获取胶囊按钮信息(仅小程序),计算header位置
|
||||
try {
|
||||
@@ -602,7 +632,7 @@
|
||||
})
|
||||
|
||||
// 添加用户消息(包含文件)
|
||||
const userMessage: ChatMessageItem = {
|
||||
const userMessage : ChatMessageItem = {
|
||||
type: 'user',
|
||||
content: text,
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
@@ -640,7 +670,7 @@
|
||||
}
|
||||
|
||||
// 准备流式对话(包含文件)
|
||||
const prepareData: ChatPrepareData = {
|
||||
const prepareData : ChatPrepareData = {
|
||||
chatId: chatId.value,
|
||||
query: query,
|
||||
agentId: agentId,
|
||||
@@ -776,7 +806,7 @@
|
||||
}
|
||||
|
||||
// 检查并获取设备代码
|
||||
function checkDeviceCode(action: 'workcase' | 'human') {
|
||||
function checkDeviceCode(action : 'workcase' | 'human') {
|
||||
if (!deviceCode.value) {
|
||||
// 如果没有设备代码,显示输入弹窗
|
||||
pendingAction.value = action
|
||||
@@ -881,7 +911,7 @@
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error : any) {
|
||||
uni.hideLoading()
|
||||
console.error('创建聊天室失败:', error)
|
||||
uni.showToast({
|
||||
@@ -891,6 +921,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 显示操作选择弹窗
|
||||
function showOperationSelect(type : 'chat' | 'workcase') {
|
||||
operationSelectType.value = type
|
||||
operationSelectTitle.value = type === 'chat' ? '选择聊天室操作' : '选择工单操作'
|
||||
showOperationSelectDialog.value = true
|
||||
}
|
||||
|
||||
// 处理操作选择
|
||||
function handleOperationSelect(operation : 'existing' | 'new') {
|
||||
showOperationSelectDialog.value = false
|
||||
pendingOperation.value = operation
|
||||
|
||||
if (operation === 'existing') {
|
||||
// 进入已有
|
||||
if (operationSelectType.value === 'chat') {
|
||||
// 进入已有聊天室
|
||||
goToChatRoomList()
|
||||
} else {
|
||||
// 进入已有工单
|
||||
goToWorkList()
|
||||
}
|
||||
} else {
|
||||
// 创建新的
|
||||
if (operationSelectType.value === 'chat') {
|
||||
// 创建新聊天室(联系人工)
|
||||
contactHuman()
|
||||
} else {
|
||||
// 创建新工单
|
||||
showCreator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消操作选择
|
||||
function cancelOperationSelect() {
|
||||
showOperationSelectDialog.value = false
|
||||
operationSelectType.value = 'chat'
|
||||
operationSelectTitle.value = ''
|
||||
pendingOperation.value = ''
|
||||
}
|
||||
|
||||
// 直接跳转到工单详情页的 create 模式(复用 workcaseDetail 页面)
|
||||
async function showCreator() {
|
||||
// 检查设备代码
|
||||
@@ -1023,7 +1094,7 @@
|
||||
sourceType: ['album'],
|
||||
success: (res) => {
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
res.tempFilePaths.forEach((filePath: string) => {
|
||||
res.tempFilePaths.forEach((filePath : string) => {
|
||||
uploadSingleFile(filePath)
|
||||
})
|
||||
}
|
||||
@@ -1039,15 +1110,15 @@
|
||||
count: 5,
|
||||
type: 'file',
|
||||
extension: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'txt'],
|
||||
success: (res: any) => {
|
||||
success: (res : any) => {
|
||||
console.log('选择文件成功:', res)
|
||||
if (res.tempFiles && res.tempFiles.length > 0) {
|
||||
res.tempFiles.forEach((file: any) => {
|
||||
res.tempFiles.forEach((file : any) => {
|
||||
uploadSingleFile(file.path)
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err: any) => {
|
||||
fail: (err : any) => {
|
||||
console.error('选择文件失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择文件失败',
|
||||
@@ -1065,15 +1136,15 @@
|
||||
uni.chooseFile({
|
||||
count: 5,
|
||||
extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt'],
|
||||
success: (res: any) => {
|
||||
success: (res : any) => {
|
||||
console.log('选择文件成功:', res)
|
||||
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
||||
res.tempFilePaths.forEach((filePath: string) => {
|
||||
res.tempFilePaths.forEach((filePath : string) => {
|
||||
uploadSingleFile(filePath)
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err: any) => {
|
||||
fail: (err : any) => {
|
||||
console.error('选择文件失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择文件失败',
|
||||
@@ -1091,7 +1162,7 @@
|
||||
}
|
||||
|
||||
// 上传单个文件
|
||||
async function uploadSingleFile(filePath: string) {
|
||||
async function uploadSingleFile(filePath : string) {
|
||||
console.log('开始上传文件:', filePath)
|
||||
|
||||
if (!agentId) {
|
||||
@@ -1112,7 +1183,7 @@
|
||||
} else {
|
||||
uni.showToast({ title: result.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error : any) {
|
||||
console.error('文件上传失败:', error)
|
||||
uni.showToast({ title: '上传失败: ' + (error.message || '未知错误'), icon: 'none' })
|
||||
} finally {
|
||||
@@ -1122,27 +1193,27 @@
|
||||
}
|
||||
|
||||
// 移除已上传的文件
|
||||
function removeUploadedFile(index: number) {
|
||||
function removeUploadedFile(index : number) {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 判断是否为图片文件
|
||||
function isImageFile(file: DifyFileInfo): boolean {
|
||||
function isImageFile(file : DifyFileInfo) : boolean {
|
||||
return file.type === 'image' || file.mime_type?.startsWith('image/') || false
|
||||
}
|
||||
|
||||
// 获取文件预览URL
|
||||
function getFilePreviewUrl(file: DifyFileInfo): string {
|
||||
function getFilePreviewUrl(file : DifyFileInfo) : string {
|
||||
return file.preview_url || file.source_url || file.url || ''
|
||||
}
|
||||
|
||||
// 获取文件下载URL(通过文件ID)
|
||||
function getFileDownloadUrl(fileId: string): string {
|
||||
function getFileDownloadUrl(fileId : string) : string {
|
||||
return fileAPI.getDownloadUrl(fileId)
|
||||
}
|
||||
|
||||
// 判断文件ID对应的文件是否为图片
|
||||
function isImageFileById(fileId: string): boolean {
|
||||
function isImageFileById(fileId : string) : boolean {
|
||||
// 从缓存中查找文件信息
|
||||
const file = fileInfoCache.value.get(fileId)
|
||||
if (file) {
|
||||
@@ -1157,13 +1228,13 @@
|
||||
}
|
||||
|
||||
// 获取文件名(从缓存)
|
||||
function getFileName(fileId: string): string {
|
||||
function getFileName(fileId : string) : string {
|
||||
const file = fileInfoCache.value.get(fileId)
|
||||
return file?.name || fileId.substring(0, 8) + '...'
|
||||
}
|
||||
|
||||
// 文件预览
|
||||
function previewFile(fileId: string) {
|
||||
function previewFile(fileId : string) {
|
||||
const url = getFileDownloadUrl(fileId)
|
||||
// 如果是图片,使用图片预览
|
||||
if (isImageFileById(fileId)) {
|
||||
@@ -1191,7 +1262,6 @@
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { workcaseChatAPI } from '@/api/workcase'
|
||||
import { MEET_URL } from '@/config'
|
||||
|
||||
const statusBarHeight = ref(44)
|
||||
const navBarHeight = ref(88)
|
||||
@@ -288,8 +289,24 @@ function copyUrl() {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析iframeUrl并构建完整的会议链接
|
||||
let fullMeetingUrl = meetingUrl.value
|
||||
|
||||
// 检查是否为微信小程序环境
|
||||
// #ifdef MP-WEIXIN
|
||||
// 获取页面参数中的iframeUrl
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
const iframeUrl = currentPage?.options?.iframeUrl || ''
|
||||
|
||||
if (iframeUrl) {
|
||||
// 构建正确的会议链接:MEET_URL + iframeUrl
|
||||
fullMeetingUrl = `${MEET_URL}${iframeUrl}`
|
||||
}
|
||||
// #endif
|
||||
|
||||
uni.setClipboardData({
|
||||
data: meetingUrl.value,
|
||||
data: fullMeetingUrl,
|
||||
success: () => {
|
||||
uni.showToast({ title: '链接已复制', icon: 'success' })
|
||||
},
|
||||
@@ -306,10 +323,26 @@ function openInBrowser() {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析iframeUrl并构建完整的会议链接
|
||||
let fullMeetingUrl = meetingUrl.value
|
||||
|
||||
// 检查是否为微信小程序环境且会议链接不是完整的MEET_URL格式
|
||||
// #ifdef MP-WEIXIN
|
||||
// 获取页面参数中的iframeUrl
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1] as any
|
||||
const iframeUrl = currentPage?.options?.iframeUrl || ''
|
||||
|
||||
if (iframeUrl) {
|
||||
// 构建正确的会议链接:MEET_URL + iframeUrl
|
||||
fullMeetingUrl = `${MEET_URL}${iframeUrl}`
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序:先复制链接,然后提示用户通过右上角菜单在浏览器中打开
|
||||
uni.setClipboardData({
|
||||
data: meetingUrl.value,
|
||||
data: fullMeetingUrl,
|
||||
success: () => {
|
||||
uni.showModal({
|
||||
title: '链接已复制',
|
||||
@@ -329,10 +362,10 @@ function openInBrowser() {
|
||||
// @ts-ignore
|
||||
if (typeof plus !== 'undefined') {
|
||||
// App环境:使用plus打开系统浏览器
|
||||
plus.runtime.openURL(meetingUrl.value)
|
||||
plus.runtime.openURL(fullMeetingUrl)
|
||||
} else {
|
||||
// H5环境:新窗口打开
|
||||
window.open(meetingUrl.value, '_blank')
|
||||
window.open(fullMeetingUrl, '_blank')
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
@@ -2,34 +2,53 @@
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<view class="nav-back-icon"></view>
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
|
||||
<view class="nav-back" @tap="goBack">
|
||||
<view class="nav-back-icon"></view>
|
||||
</view>
|
||||
<text class="nav-title">我的工单</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
<text class="nav-title">我的工单</text>
|
||||
<view class="nav-capsule"></view>
|
||||
</view>
|
||||
|
||||
<!-- Tab切换 -->
|
||||
<view class="tabs" :style="{ marginTop: headerTotalHeight + 'px' }">
|
||||
<view class="tab-item" :class="{ active: activeTab === 'all' }" @tap="changeTab('all')">
|
||||
<text class="tab-text">全部</text>
|
||||
<!-- Tab切换 -->
|
||||
<view class="tabs" :style="{ marginTop: headerTotalHeight + 'px' }">
|
||||
<view class="tab-item" :class="{ active: activeTab === 'all' }" @tap="changeTab('all')">
|
||||
<text class="tab-text">全部</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'pending' }" @tap="changeTab('pending')">
|
||||
<text class="tab-text">待处理</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'processing' }" @tap="changeTab('processing')">
|
||||
<text class="tab-text">处理中</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'done' }" @tap="changeTab('done')">
|
||||
<text class="tab-text">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'pending' }" @tap="changeTab('pending')">
|
||||
<text class="tab-text">待处理</text>
|
||||
<!-- 用户筛选组件(仅非guest用户可见) -->
|
||||
<view class="filter-section" v-if="!isGuest" :style="{ marginTop: headerTotalHeight + 44 + 'px' }">
|
||||
<view class="filter-label">选择用户:</view>
|
||||
<view class="filter-content">
|
||||
<view class="guest-selector" @tap="showGuestSelector = true">
|
||||
<text v-if="selectedGuestId" class="selected-guest">
|
||||
{{ guestsList.find(g => g.userId === selectedGuestId)?.name || '请选择用户' }}
|
||||
</text>
|
||||
<text v-else class="placeholder">请选择用户</text>
|
||||
<view class="selector-arrow">▼</view>
|
||||
</view>
|
||||
<view class="btn-group">
|
||||
<view class="reset-btn" @tap="clearGuestSelect">重置</view>
|
||||
<view class="query-btn" @tap="handleQuery">查询</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'processing' }" @tap="changeTab('processing')">
|
||||
<text class="tab-text">处理中</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'done' }" @tap="changeTab('done')">
|
||||
<text class="tab-text">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 工单列表 -->
|
||||
<scroll-view class="list" scroll-y="true">
|
||||
<!-- 工单列表 -->
|
||||
<scroll-view class="list" scroll-y="true"
|
||||
:style="{ marginTop: headerTotalHeight + 44 + (isGuest ? 0 : 44) + 'px' }"
|
||||
@scrolltolower="handleScrollToLower">
|
||||
<view class="card" v-for="(item, index) in filteredOrders" :key="index">
|
||||
<view class="card-header">
|
||||
<view class="card-title">
|
||||
@@ -74,128 +93,569 @@
|
||||
<view class="empty-state" v-if="filteredOrders.length === 0">
|
||||
<text class="empty-text">暂无工单数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多提示 -->
|
||||
<view class="load-more" v-if="loading">
|
||||
<text class="load-text">加载中...</text>
|
||||
</view>
|
||||
<view class="load-more" v-else-if="!hasMore && filteredOrders.length > 0">
|
||||
<text class="load-text">没有更多数据了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 用户选择弹窗 -->
|
||||
<view class="modal-mask" v-if="showGuestSelector" @tap="showGuestSelector = false"></view>
|
||||
<view class="modal-content" v-if="showGuestSelector">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">选择用户</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="guest-item" v-for="guest in guestsList" :key="guest.userId"
|
||||
@tap="handleGuestSelect(guest)">
|
||||
<text class="guest-name">{{ guest.name }} {{ guest.phone || '无电话' }}</text>
|
||||
</view>
|
||||
<view class="empty-item" v-if="guestsList.length === 0">
|
||||
<text>暂无可选用户</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import type { TbWorkcaseDTO } from '@/types/workcase'
|
||||
import { workcaseAPI } from '@/api/workcase/workcase'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import type { TbWorkcaseDTO, TbGuestDTO, PageRequest, PageParam } from '@/types'
|
||||
import { workcaseAPI } from '@/api/workcase/workcase'
|
||||
import { guestAPI } from '@/api/sys/guest'
|
||||
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
const activeTab = ref<string>('all')
|
||||
const orders = ref<TbWorkcaseDTO[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const error = ref<string>('')
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
const activeTab = ref<string>('all')
|
||||
const orders = ref<TbWorkcaseDTO[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const loadingUsers = ref<boolean>(false)
|
||||
const error = ref<string>('')
|
||||
|
||||
// 计算属性:根据tab筛选工单
|
||||
const filteredOrders = computed(() => {
|
||||
if (activeTab.value === 'all') {
|
||||
// 分页相关
|
||||
const currentPage = ref<number>(1)
|
||||
const pageSize = ref<number>(10)
|
||||
const total = ref<number>(0)
|
||||
const hasMore = ref<boolean>(true)
|
||||
|
||||
// 用户筛选相关
|
||||
const isGuest = ref(true)
|
||||
const guestsList = ref<TbGuestDTO[]>([])
|
||||
const selectedGuestId = ref<string>('')
|
||||
const showGuestSelector = ref<boolean>(false)
|
||||
|
||||
// 计算属性:直接返回分页数据(API已处理筛选)
|
||||
const filteredOrders = computed(() => {
|
||||
return orders.value
|
||||
}
|
||||
return orders.value.filter(o => o.status === activeTab.value)
|
||||
})
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
const statusBarHeight = windowInfo.statusBarHeight || 44
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
headerPaddingTop.value = menuButtonInfo.top
|
||||
headerTotalHeight.value = menuButtonInfo.bottom + 8
|
||||
} catch (e) {
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
const statusBarHeight = windowInfo.statusBarHeight || 44
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
headerPaddingTop.value = menuButtonInfo.top
|
||||
headerTotalHeight.value = menuButtonInfo.bottom + 8
|
||||
} catch (e) {
|
||||
headerPaddingTop.value = statusBarHeight
|
||||
headerTotalHeight.value = statusBarHeight + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
headerPaddingTop.value = statusBarHeight
|
||||
headerTotalHeight.value = statusBarHeight + 44
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
headerPaddingTop.value = statusBarHeight
|
||||
headerTotalHeight.value = statusBarHeight + 44
|
||||
// #endif
|
||||
|
||||
// 调用API获取工单列表
|
||||
loadWorkcaseList()
|
||||
})
|
||||
// #endif
|
||||
|
||||
// 加载工单列表
|
||||
async function loadWorkcaseList() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const filter: TbWorkcaseDTO = {}
|
||||
if (activeTab.value !== 'all') {
|
||||
filter.status = activeTab.value as TbWorkcaseDTO['status']
|
||||
// 检查用户类型
|
||||
checkUserType()
|
||||
|
||||
// 调用API获取工单列表
|
||||
loadWorkcaseList()
|
||||
})
|
||||
|
||||
// 检查用户类型
|
||||
function checkUserType() {
|
||||
try {
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
const parsedUserInfo = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
|
||||
isGuest.value = parsedUserInfo.status === 'guest'
|
||||
|
||||
// 如果是非guest用户,加载可选人员列表
|
||||
if (!isGuest.value) {
|
||||
loadGuestsList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查用户类型失败:', error)
|
||||
isGuest.value = true
|
||||
}
|
||||
const res = await workcaseAPI.getWorkcaseList(filter)
|
||||
if (res.success && res.dataList) {
|
||||
orders.value = res.dataList || []
|
||||
} else {
|
||||
error.value = res.message || '加载失败'
|
||||
}
|
||||
|
||||
// 加载可选人员列表
|
||||
async function loadGuestsList() {
|
||||
loadingUsers.value = true
|
||||
try {
|
||||
const res = await guestAPI.listGuest()
|
||||
if (res.success && res.dataList) {
|
||||
guestsList.value = res.dataList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载可选人员列表失败:', error)
|
||||
} finally {
|
||||
loadingUsers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载工单列表 - 滚动分页查询
|
||||
async function loadWorkcaseList(isLoadMore = false) {
|
||||
// 如果正在加载或没有更多数据,直接返回
|
||||
if (loading.value || (!isLoadMore && !hasMore.value)) return
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const filter: TbWorkcaseDTO = {}
|
||||
if (activeTab.value !== 'all') {
|
||||
filter.status = activeTab.value as TbWorkcaseDTO['status']
|
||||
}
|
||||
|
||||
// 如果是非guest用户且选择了特定用户,添加userId筛选
|
||||
if (!isGuest.value && selectedGuestId.value) {
|
||||
filter.userId = selectedGuestId.value
|
||||
}
|
||||
|
||||
// 计算当前页码
|
||||
const currentPageNum = isLoadMore ? currentPage.value + 1 : 1
|
||||
|
||||
const pageParam: PageParam = {
|
||||
page: currentPageNum,
|
||||
pageSize: pageSize.value
|
||||
}
|
||||
|
||||
const pageRequest: PageRequest<TbWorkcaseDTO> = {
|
||||
filter,
|
||||
pageParam
|
||||
}
|
||||
|
||||
const res = await workcaseAPI.getWorkcasePage(pageRequest)
|
||||
if (res.success) {
|
||||
const newOrders = res.dataList || res.pageDomain?.dataList || []
|
||||
total.value = res.pageDomain?.pageParam?.total || 0
|
||||
|
||||
// 根据是否加载更多来处理数据
|
||||
if (isLoadMore) {
|
||||
// 加载更多:追加数据
|
||||
orders.value = [...orders.value, ...newOrders]
|
||||
currentPage.value++
|
||||
} else {
|
||||
// 刷新:替换数据
|
||||
orders.value = newOrders
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
hasMore.value = orders.value.length < total.value
|
||||
} else {
|
||||
error.value = res.message || '加载失败'
|
||||
uni.showToast({
|
||||
title: res.message || '加载失败',
|
||||
icon: 'error'
|
||||
})
|
||||
// 如果是加载更多失败,保持hasMore不变
|
||||
if (!isLoadMore) {
|
||||
hasMore.value = false
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = '网络错误,请稍后重试'
|
||||
uni.showToast({
|
||||
title: res.message || '加载失败',
|
||||
title: '网络错误,请稍后重试',
|
||||
icon: 'error'
|
||||
})
|
||||
// 如果是加载更多失败,保持hasMore不变
|
||||
if (!isLoadMore) {
|
||||
hasMore.value = false
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = '网络错误,请稍后重试'
|
||||
uni.showToast({
|
||||
title: '网络错误,请稍后重试',
|
||||
icon: 'error'
|
||||
}
|
||||
|
||||
// 处理滚动到底部事件
|
||||
function handleScrollToLower() {
|
||||
if (hasMore.value && !loading.value) {
|
||||
loadWorkcaseList(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理用户选择
|
||||
function handleGuestSelect(guest : TbGuestDTO) {
|
||||
selectedGuestId.value = guest.userId || ''
|
||||
showGuestSelector.value = false
|
||||
// 重置分页状态
|
||||
hasMore.value = true
|
||||
loadWorkcaseList() // 根据选中的用户重新加载工单列表
|
||||
}
|
||||
|
||||
// 清除用户选择
|
||||
function clearGuestSelect() {
|
||||
selectedGuestId.value = ''
|
||||
// 重置分页状态
|
||||
hasMore.value = true
|
||||
loadWorkcaseList() // 重新加载所有工单列表
|
||||
}
|
||||
|
||||
// 查询工单列表
|
||||
function handleQuery() {
|
||||
// 重置分页状态
|
||||
hasMore.value = true
|
||||
loadWorkcaseList() // 根据当前筛选条件重新加载工单列表
|
||||
}
|
||||
|
||||
// 切换Tab
|
||||
function changeTab(tab : string) {
|
||||
activeTab.value = tab
|
||||
// 重置分页状态
|
||||
hasMore.value = true
|
||||
loadWorkcaseList()
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
function getStatusClass(status ?: string) : string {
|
||||
switch (status) {
|
||||
case 'pending': return 'status-pending'
|
||||
case 'processing': return 'status-processing'
|
||||
case 'done': return 'status-done'
|
||||
default: return 'status-pending'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status ?: string) : string {
|
||||
switch (status) {
|
||||
case 'pending': return '待处理'
|
||||
case 'processing': return '处理中'
|
||||
case 'done': return '已完成'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 跳转到工单详情
|
||||
function goDetail(workcaseId ?: string) {
|
||||
if (!workcaseId) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/workcase/workcaseDetail/workcaseDetail?workcaseId=${workcaseId}`
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换Tab
|
||||
function changeTab(tab: string) {
|
||||
activeTab.value = tab
|
||||
loadWorkcaseList()
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
function getStatusClass(status?: string): string {
|
||||
switch (status) {
|
||||
case 'pending': return 'status-pending'
|
||||
case 'processing': return 'status-processing'
|
||||
case 'done': return 'status-done'
|
||||
default: return 'status-pending'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status?: string): string {
|
||||
switch (status) {
|
||||
case 'pending': return '待处理'
|
||||
case 'processing': return '处理中'
|
||||
case 'done': return '已完成'
|
||||
default: return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 跳转到工单详情
|
||||
function goDetail(workcaseId?: string) {
|
||||
if (!workcaseId) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/workcase/workcaseDetail/workcaseDetail?workcaseId=${workcaseId}`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./workcaseList.scss";
|
||||
@import "./workcaseList.scss";
|
||||
|
||||
// 用户筛选组件样式
|
||||
.filter-section {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 44px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.guest-selector {
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
height: 32px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 16px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f9f9f9;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.guest-selector:active {
|
||||
background: #e9e9e9;
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
.selected-guest {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.selector-arrow {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-section:active .selector-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 按钮组样式 */
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.reset-btn,
|
||||
.query-btn {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.reset-btn:active {
|
||||
background: #e9e9e9;
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
background: #1989fa;
|
||||
border: 1px solid #1989fa;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.query-btn:active {
|
||||
background: #0066cc;
|
||||
border-color: #0066cc;
|
||||
}
|
||||
|
||||
// 弹窗样式
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.25);
|
||||
z-index: 1001;
|
||||
animation: slideUp 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 弹窗动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -45%);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.modal-close:active {
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.modal-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
.guest-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.guest-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.guest-item:active {
|
||||
background-color: #f8f8f8;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.guest-item:active .guest-name {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.guest-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.guest-phone {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.guest-phone::before {
|
||||
content: "📞";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty-item {
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.empty-item::before {
|
||||
content: "👤";
|
||||
font-size: 48px;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 加载更多样式 */
|
||||
.load-more {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.load-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user