Compare commits

..

22 Commits

Author SHA1 Message Date
huxuejian
24c6c7b2fe fix: 问题修复 2026-03-06 14:56:22 +08:00
huxuejian
99d022448d fix: 问题修复 2026-03-05 18:16:07 +08:00
huxuejian
85d4ec8996 Update api.js 2026-03-05 17:46:49 +08:00
huxuejian
dbd91fb408 fix: 问题修复 2026-03-05 17:04:49 +08:00
huxuejian
238d8159ed fix: 问题修复 2026-03-05 15:03:07 +08:00
huxuejian
5c14cc7e5f Update survey-list.vue 2026-03-05 10:31:35 +08:00
huxuejian
2beadec667 Update homepage.vue 2026-03-04 17:59:49 +08:00
huxuejian
e2d6a964bf fix: 问题修复 2026-03-04 17:19:52 +08:00
huxuejian
51a184f11e feat: 小程序首页新增查询待处理角标显示 2026-03-04 10:49:25 +08:00
huxuejian
b9c12e0deb fix: 阅读记录调整 2026-03-03 15:46:13 +08:00
huxuejian
a1c80166af Update rate-list.vue 2026-02-28 18:02:02 +08:00
huxuejian
bba3cfbe4c feat: 服务评价开发 2026-02-28 15:08:23 +08:00
huxuejian
8e711875af feat: 新增问卷填写功能 2026-02-28 11:18:20 +08:00
huxuejian
5fbb442ff5 fix: 问题修复 2026-02-27 10:36:17 +08:00
huxuejian
39847cbad4 Merge remote-tracking branch 'origin/dev-wdb' 2026-02-12 15:16:19 +08:00
huxuejian
94c09366bc fix: 问题修复 2026-02-12 15:16:09 +08:00
a01194c726 Merge commit 'dc7d1b082a2915635afb2d9b4e96d842744304fd' into dev-wdb 2026-02-12 15:06:58 +08:00
253970b860 no message 2026-02-12 15:06:49 +08:00
huxuejian
dc7d1b082a Update edit-archive.vue 2026-02-12 15:06:33 +08:00
f2bad7b3de Merge commit '932ec63261d20bca7b811367d7aa1ab2abcee391' into dev-wdb 2026-02-12 12:05:47 +08:00
6b9d720134 no message 2026-02-12 12:05:35 +08:00
e71caa6d69 no message 2026-02-12 12:04:57 +08:00
30 changed files with 1880 additions and 366 deletions

14
App.vue
View File

@ -40,7 +40,7 @@ export default {
if (success) {
console.log("IM 初始化成功");
// IM
globalUnreadListenerManager.setup();
// globalUnreadListenerManager.setup();
} else {
console.warn("IM 初始化失败");
}
@ -76,6 +76,17 @@ page {
position: relative;
}
.absolute {
position: absolute;
}
.inset-0 {
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.inline-block {
display: inline-block;
}
@ -180,6 +191,7 @@ page {
.pt-5 {
padding-top: 10rpx;
}
.pt-15 {
padding-top: 30rpx;
}

33
hooks/usePageList.js Normal file
View File

@ -0,0 +1,33 @@
import { computed, ref, watch } from "vue";
import useDebounce from '@/utils/useDebounce'
export default function usePageList(callback, options = {}) {
const keyword = ref('')
const list = ref([])
const page = ref(1)
const pageSize = ref(options.pageSize || 20)
const pages = ref(0);
const loading = ref(false)
const total = ref(0)
const hasMore = computed(() => page.value < pages.value)
const handleKeywordChange = useDebounce(() => {
getList()
}, options.debounce || 1000)
function changePage(p) {
if (loading.value) return
page.value = p
getList()
}
function getList() {
typeof callback === 'function' && callback()
}
watch(keyword, handleKeywordChange);
return { total, page, pageSize, keyword, list, pages, changePage, loading, hasMore }
}

View File

@ -1,215 +1,236 @@
{
"pages": [
{
"path": "pages/home/home",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/article/article-list",
"style": {
"navigationBarTitleText": "我的宣教",
"disableScroll": true
}
},
{
"path": "pages/article/article-cate-list",
"style": {
"navigationBarTitleText": "健康宣教",
"disableScroll": true
}
},
{
"path": "pages/survey/survey-list",
"style": {
"navigationBarTitleText": "我的问卷",
"disableScroll": true
}
},
{
"path": "pages/message/message",
"style": {
"navigationBarTitleText": "消息",
"disableScroll": true
}
},
{
"path": "pages/message/index",
"style": {
"navigationBarTitleText": "聊天",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "健康柚",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/login/redirect-page",
"style": {
"navigationBarTitleText": "健康柚",
"disableScroll": true
}
},
{
"path": "pages/login/agreement",
"style": {
"navigationBarTitleText": "健康柚",
"disableScroll": true
}
},
{
"path": "pages/archive/archive-manage",
"style": {
"navigationBarTitleText": "档案管理",
"disableScroll": true
}
},
{
"path": "pages/archive/edit-archive",
"style": {
"navigationBarTitleText": "新增档案",
"disableScroll": true
}
},
{
"path": "pages/archive/archive-result",
"style": {
"navigationBarTitleText": "团队服务",
"disableScroll": true
}
},
{
"path": "pages/health/list",
"style": {
"navigationBarTitleText": "健康信息",
"disableScroll": true
}
},
{
"path": "pages/health/record",
"style": {
"navigationBarTitleText": "健康信息",
"disableScroll": true
}
},
{
"path": "pages/library/diagnosis-list",
"style": {
"navigationBarTitleText": "选择诊断",
"disableScroll": true
}
},
{
"path": "pages/team/team-detail",
"style": {
"navigationBarTitleText": "团队介绍",
"disableScroll": true
}
},
{
"path": "pages/team/homepage",
"style": {
"navigationBarTitleText": "个人主页",
"disableScroll": true
}
},
{
"path": "pages/team/friend",
"style": {
"navigationBarTitleText": "添加好友",
"disableScroll": true
}
},
{
"path": "pages/web-view/web-view",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
},
{
"path": "pages/mine/mine",
"style": {
"navigationBarTitleText": "我的",
"disableScroll": true
}
},
{
"path": "pages/mine/contact",
"style": {
"navigationBarTitleText": "联系客服",
"disableScroll": true
}
},
{
"path": "pages/common/privacy",
"style": {
"navigationBarTitleText": "隐私政策",
"disableScroll": true
}
},
{
"path": "pages/common/agreement",
"style": {
"navigationBarTitleText": "用户协议",
"disableScroll": true
}
},
{
"path": "pages/article/article-detail",
"style": {
"navigationBarTitleText": "宣教文章",
"disableScroll": true
}
},
{
"path": "pages/article/send-article",
"style": {
"navigationBarTitleText": "选择宣教文章",
"disableScroll": true
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#065bd6",
"backgroundColor": "#065bd6"
},
"tabBar": {
"color": "#666666",
"selectedColor": "#007aff",
"backgroundColor": "#ffffff",
"borderStyle": "white",
"list": [
{
"pagePath": "pages/home/home",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home_selected.png",
"text": "服务"
},
{
"pagePath": "pages/message/message",
"iconPath": "static/tabbar/consult.png",
"selectedIconPath": "static/tabbar/consult_selected.png",
"text": "咨询"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine_selected.png",
"text": "我的"
}
]
},
"uniIdRouter": {}
}
"pages": [
{
"path": "pages/home/home",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/article/article-list",
"style": {
"navigationBarTitleText": "我的宣教",
"disableScroll": true
}
},
{
"path": "pages/article/article-cate-list",
"style": {
"navigationBarTitleText": "健康宣教",
"disableScroll": true
}
},
{
"path": "pages/survey/survey-list",
"style": {
"navigationBarTitleText": "我的问卷",
"disableScroll": true
}
},
{
"path": "pages/survey/fill",
"style": {
"navigationBarTitleText": "问卷",
"disableScroll": true
}
},
{
"path": "pages/rate/rate-list",
"style": {
"navigationBarTitleText": "服务评价",
"disableScroll": true
}
},
{
"path": "pages/rate/rate-detail",
"style": {
"navigationBarTitleText": "服务评价",
"disableScroll": true
}
},
{
"path": "pages/message/message",
"style": {
"navigationBarTitleText": "消息",
"disableScroll": true
}
},
{
"path": "pages/message/index",
"style": {
"navigationBarTitleText": "聊天",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "健康柚",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/login/redirect-page",
"style": {
"navigationBarTitleText": "健康柚",
"disableScroll": true
}
},
{
"path": "pages/login/agreement",
"style": {
"navigationBarTitleText": "健康柚",
"disableScroll": true
}
},
{
"path": "pages/archive/archive-manage",
"style": {
"navigationBarTitleText": "档案管理",
"disableScroll": true
}
},
{
"path": "pages/archive/edit-archive",
"style": {
"navigationBarTitleText": "新增档案",
"disableScroll": true
}
},
{
"path": "pages/archive/archive-result",
"style": {
"navigationBarTitleText": "团队服务",
"disableScroll": true
}
},
{
"path": "pages/health/list",
"style": {
"navigationBarTitleText": "健康信息",
"disableScroll": true
}
},
{
"path": "pages/health/record",
"style": {
"navigationBarTitleText": "健康信息",
"disableScroll": true
}
},
{
"path": "pages/library/diagnosis-list",
"style": {
"navigationBarTitleText": "选择诊断",
"disableScroll": true
}
},
{
"path": "pages/team/team-detail",
"style": {
"navigationBarTitleText": "团队介绍",
"disableScroll": true
}
},
{
"path": "pages/team/homepage",
"style": {
"navigationBarTitleText": "个人主页",
"disableScroll": true
}
},
{
"path": "pages/team/friend",
"style": {
"navigationBarTitleText": "添加好友",
"disableScroll": true
}
},
{
"path": "pages/web-view/web-view",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
},
{
"path": "pages/mine/mine",
"style": {
"navigationBarTitleText": "我的",
"disableScroll": true
}
},
{
"path": "pages/mine/contact",
"style": {
"navigationBarTitleText": "联系客服",
"disableScroll": true
}
},
{
"path": "pages/common/privacy",
"style": {
"navigationBarTitleText": "隐私政策",
"disableScroll": true
}
},
{
"path": "pages/common/agreement",
"style": {
"navigationBarTitleText": "用户协议",
"disableScroll": true
}
},
{
"path": "pages/article/article-detail",
"style": {
"navigationBarTitleText": "宣教文章",
"disableScroll": true
}
},
{
"path": "pages/article/send-article",
"style": {
"navigationBarTitleText": "选择宣教文章",
"disableScroll": true
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#065bd6",
"backgroundColor": "#065bd6"
},
"tabBar": {
"color": "#666666",
"selectedColor": "#007aff",
"backgroundColor": "#ffffff",
"borderStyle": "white",
"list": [
{
"pagePath": "pages/home/home",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home_selected.png",
"text": "服务"
},
{
"pagePath": "pages/message/message",
"iconPath": "static/tabbar/consult.png",
"selectedIconPath": "static/tabbar/consult_selected.png",
"text": "咨询"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine_selected.png",
"text": "我的"
}
]
},
"uniIdRouter": {}
}

View File

@ -58,12 +58,13 @@ const { useLoad, useShow } = useGuard();
const { account } = storeToRefs(useAccount());
const corpId = ref('');
const teamId = ref('');
const corpUserId = ref('')
const enableHis = ref(false);
const customers = ref([]);
function addArchive() {
uni.navigateTo({
url: `/pages/archive/edit-archive?teamId=${teamId.value}&corpId=${corpId.value}`
url: `/pages/archive/edit-archive?corpUserId=${corpUserId.value}&teamId=${teamId.value}&corpId=${corpId.value}`
})
}
@ -96,6 +97,7 @@ async function unBindArchive(customer) {
useLoad(options => {
teamId.value = options.teamId;
corpId.value = options.corpId;
corpUserId.value = options.corpUserId;
})
useShow(() => {

View File

@ -43,6 +43,7 @@ const { account, externalUserId } = storeToRefs(useAccount());
const { getExternalUserId } = useAccount()
const corpId = ref('');
const corpName = ref('');
const corpUserId = ref('');
const customer = ref({});
const customerId = ref('');
const customers = ref([]);
@ -52,7 +53,6 @@ const formItems = ref([]);
const loading = ref(false);
const teamId = ref('');
const tempRef = ref(null);
const customerArchive = ref(null);
const verifyVisible = ref(false);
const visible = ref(false);
@ -64,14 +64,19 @@ function change({ title, value }) {
if (title) {
form.value[title] = value;
}
if (title !== 'idCard') return;
const [isIdCard, birthday, gender] = validate.isChinaId(value);
if (isIdCard) {
form.value.birthday = birthday;
form.value.sex = gender == 'MALE' ? '男' : '女';
const age = dayjs().diff(birthday, 'year');
if (title == 'idCard') {
const [isIdCard, birthday, gender] = validate.isChinaId(value);
if (isIdCard) {
form.value.birthday = birthday;
form.value.sex = gender == 'MALE' ? '男' : '女';
const age = dayjs().diff(birthday, 'year');
form.value.age = Math.max(1, age);
}
} else if (title === 'birthday' && formItems.value.some(i => i.title === 'age') && value && dayjs(value).valueOf()) {
const age = dayjs().diff(value, 'year');
form.value.age = Math.max(1, age);
}
}
function confirm() {
@ -136,10 +141,15 @@ async function addArchive() {
externalUserId: externalUserId.value,
realUnionid: account.value.unionid || '',
}
if (externalUserId.value) {
const corpUserId = await getResponsiblePerson();
if (corpUserId) {
params.personResponsibles = [{ corpUserId, teamId: teamId.value }]
}
}
loading.value = false;
const res = await api('addCustomer', { params });
if (res && res.success) {
set('home-invite-teamId', teamId.value);
uni.$emit('reloadTeamCustomers')
uni.redirectTo({
url: `/pages/archive/archive-result?corpId=${corpId.value}&teamId=${teamId.value}`
@ -149,11 +159,21 @@ async function addArchive() {
}
}
async function getResponsiblePerson() {
const res = await api('getResponsiblePerson', { corpId: corpId.value, teamId: teamId.value, externalUserId: externalUserId.value, corpUserId: corpUserId.value });
return res && res.data ? res.data : ''
}
async function bindArchive(customerId) {
const res = await api('bindMiniAppArchive', { id: customerId, corpId: corpId.value, teamId: teamId.value, miniAppId: account.value.openid });
let responsiblePerson = '';
if (externalUserId.value) {
const corpUserId = await getResponsiblePerson();
responsiblePerson = corpUserId || '';
}
const res = await api('bindMiniAppArchive', { id: customerId, corpId: corpId.value, teamId: teamId.value, miniAppId: account.value.openid, externalUserId: externalUserId.value, responsiblePerson });
if (res && res.success) {
await toast('绑定成功');
set('home-invite-teamId', teamId.value);
uni.$emit('reloadTeamCustomers')
uni.switchTab({
url: '/pages/home/home'
})
@ -262,6 +282,7 @@ async function unBindArchive() {
onLoad(options => {
customerId.value = options.id || '';
corpUserId.value = options.corpUserId || '';
uni.setNavigationBarTitle({ title: customerId.value ? '编辑档案' : '新增档案' })
})

View File

@ -1,31 +1,34 @@
<template>
<view class="article-detail-page">
<view v-if="loading" class="loading-container">
<uni-icons type="spinner-cycle" size="40" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="error" class="error-container">
<text class="error-text">{{ error }}</text>
<button class="retry-btn" @click="loadArticle">重试</button>
</view>
<scroll-view v-else scroll-y class="article-content">
<view class="article-header">
<text class="article-title">{{ articleData.title }}</text>
<text class="article-date">{{ articleData.date }}</text>
<full-page mainClass="bg-white">
<view class="article-detail-page">
<view v-if="loading" class="loading-container">
<uni-icons type="spinner-cycle" size="40" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view class="article-body">
<view class="rich-text-wrapper">
<rich-text :nodes="articleData.content"></rich-text>
<view v-else-if="error" class="error-container">
<text class="error-text">{{ error }}</text>
<button class="retry-btn" @click="loadArticle">重试</button>
</view>
<scroll-view v-else scroll-y class="article-content">
<view class="article-header">
<text class="article-title">{{ articleData.title }}</text>
<text class="article-date">{{ articleData.date }}</text>
</view>
</view>
</scroll-view>
</view>
<view class="article-body">
<view class="rich-text-wrapper">
<rich-text :nodes="articleData.content"></rich-text>
</view>
</view>
</scroll-view>
</view>
</full-page>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import FullPage from "@/components/full-page.vue";
import api from "@/utils/api.js";
import { ref } from "vue";
import { storeToRefs } from "pinia";
@ -40,15 +43,15 @@ const articleData = ref({
});
let articleId = "";
const corpId = ref('')
const corpId = ref("");
const markArticleRead = async () => {
const markArticleRead = async (sendId) => {
const unionid = account.value?.unionid;
if (!unionid || !articleId) return;
try {
await api(
"addArticleReadRecord",
{ corpId: corpId.value, articleId, unionid },
{ corpId: corpId.value, articleId, unionid, sendId },
false
);
} catch (err) {
@ -91,7 +94,10 @@ const loadArticle = async () => {
loading.value = true;
error.value = "";
try {
const res = await api("getArticle", { id: articleId, corpId: corpId.value });
const res = await api("getArticle", {
id: articleId,
corpId: corpId.value,
});
if (res.success && res.data) {
//
@ -124,7 +130,7 @@ onLoad((options) => {
corpId.value = options.corpId;
if (options.id) {
articleId = options.id;
markArticleRead();
markArticleRead(options.sendId || '');
loadArticle();
} else {
error.value = "文章信息不完整";

View File

@ -1,18 +1,18 @@
<template>
<view class="bg-gray-100 min-h-screen">
<!-- Filter Tabs -->
<scroll-view scroll-x class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full"
:show-scrollbar="false">
<view v-for="(tab, index) in tabs" :key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors" :class="[
activeTab === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
: 'bg-white text-gray-600 border-gray-200'
]" @click="selectTab(tab.value)">
{{ tab.name }}
</view>
</scroll-view>
<full-page pageClass="bg-gray-100">
<template #header>
<scroll-view scroll-x class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full"
:show-scrollbar="false">
<view v-for="(tab, index) in tabs" :key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors" :class="[
activeTab === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
: 'bg-white text-gray-600 border-gray-200'
]" @click="selectTab(tab.value)">
{{ tab.name }}
</view>
</scroll-view>
</template>
<!-- Article List -->
<view v-if="loading && articles.length === 0" class="loading-container">
<uni-icons type="spinner-cycle" size="30" color="#999" />
@ -74,7 +74,7 @@
没有更多了
</view>
</view>
</view>
</full-page>
</template>
<script setup>
@ -84,6 +84,8 @@ import { storeToRefs } from "pinia";
import dayjs from "dayjs";
import api from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import fullPage from "@/components/full-page.vue";
import EmptyData from "@/components/empty-data.vue";
const { account, openid } = storeToRefs(useAccountStore());
@ -98,6 +100,7 @@ const pageSize = 20;
const loading = ref(false);
const inited = ref(false);
const corpId = ref('');
const teamId = ref('');
const selectTab = async (customerId) => {
if (activeTab.value === customerId) return;
@ -139,6 +142,7 @@ const mapRowToView = (row) => {
};
const loadArticleList = async (reset = false) => {
const customerIds = tabs.value.map(i => i.value).filter(Boolean);
if (loading.value) return;
const unionid = account.value?.unionid;
const miniAppId = openid.value || uni.getStorageSync("openid");
@ -152,6 +156,12 @@ const loadArticleList = async (reset = false) => {
articles.value = [];
total.value = 0;
}
if (customerIds.length === 0) {
page.value = 1;
articles.value = [];
total.value = 0;
return
}
loading.value = true;
try {
@ -161,8 +171,10 @@ const loadArticleList = async (reset = false) => {
miniAppId,
page: page.value,
pageSize,
customerIds,
teamId: teamId.value
};
if (activeTab.value) params.customerId = activeTab.value;
if (activeTab.value) params.customerIds = [activeTab.value];
const res = await api("getMiniAppReceivedArticleList", params);
if (res && res.success) {
@ -184,10 +196,11 @@ const loadArticleList = async (reset = false) => {
function goToDetail(item) {
if (!item?.articleId) return;
uni.navigateTo({ url: `/pages/article/article-detail?id=${item.articleId}&corpId=${corpId.value}` });
uni.navigateTo({ url: `/pages/article/article-detail?sendId=${item._id}&id=${item.articleId}&corpId=${corpId.value}` });
}
onLoad(opts => {
corpId.value = opts.corpId;
teamId.value = opts.teamId;
})
onShow(async () => {

View File

@ -4,8 +4,9 @@
<view class="consult-grid">
<view class="consult-item" v-for="item in consultItems" :key="item.id" @click="handleItemClick(item)">
<view class="item-icon">
<view class="relative item-icon">
<image :src="item.icon" class="icon-img" mode="aspectFill" />
<view v-if="badgeMap[item.badge]" class="item-dot"></view>
</view>
<view class="item-label">{{ item.label }}</view>
</view>
@ -18,7 +19,7 @@
</template>
<script setup>
import { ref, computed } from "vue";
import { ref, watch } from "vue";
import { storeToRefs } from "pinia";
import useAccount from "@/store/account";
import api from "@/utils/api";
@ -46,6 +47,8 @@ const props = defineProps({
const { account } = storeToRefs(useAccount());
const consultantPopup = ref(null);
const badgeMap = ref({});
let loading = false;
const consultItems = ref([
{
@ -53,24 +56,28 @@ const consultItems = ref([
label: "聊天咨询",
icon: "/static/home/chat-consult.png",
needSelectConsultant: true,
badge: 'chat'
},
{
id: "education",
label: "我的宣教",
icon: "/static/home/my-education.png",
path: "/pages/article/article-list",
badge: 'article'
},
{
id: "survey",
label: "我的问卷",
icon: "/static/home/my-questionnaire.png",
path: "/pages/survey/survey-list",
badge: 'survey'
},
{
id: "rating",
label: "服务评价",
icon: "/static/home/service-rating.png",
path: "",
path: "/pages/rate/rate-list",
badge: 'rate'
},
]);
@ -156,6 +163,32 @@ function handleAddNewArchive() {
url: `/pages/archive/edit-archive?corpId=${props.corpId}&teamId=${props.teamId}`,
});
}
async function getBadgeCount() {
if (loading) return;
loading = true;
const customerIds = props.customers.map((item) => item._id);
if (customerIds.length === 0) {
badgeMap.value = {};
loading = false
return
}
const res = await api('getMiniAppHomeStats', { corpId: props.corpId, teamId: props.teamId, customerIds }, false)
const data = res?.data || {};
const article = typeof data.article === 'number' ? data.article : 0;
const survey = typeof data.survey === 'number' ? data.survey : 0;
const rate = typeof data.rate === 'number' ? data.rate : 0;
badgeMap.value = { article, survey, rate };
loading = false;
}
watch(() => [props.customers, props.teamId, props.corpId], n => {
getBadgeCount()
}, { immediate: true })
defineExpose({
getBadgeCount,
})
</script>
<style lang="scss" scoped>
@ -204,6 +237,16 @@ function handleAddNewArchive() {
overflow: hidden;
}
.item-dot {
position: absolute;
right: 0;
top: 0;
width: 20rpx;
height: 20rpx;
background: red;
border-radius: 50%;
}
.consult-item:active .item-icon {
transform: scale(0.95);
}

View File

@ -82,6 +82,10 @@ const props = defineProps({
type: String,
default: "",
},
corpUserIds: {
type: Object,
default: () => ({}),
},
customers: {
type: Array,
default: () => [],
@ -93,8 +97,10 @@ const props = defineProps({
});
const emit = defineEmits(["update:customers"]);
let loading = false;
const { account } = storeToRefs(useAccount());
const { account, externalUserId } = storeToRefs(useAccount());
const { getExternalUserId } = useAccount()
const current = ref(null);
const customers = ref([]);
@ -142,8 +148,7 @@ function toHealthList() {
if (canAuth.value) {
toast("请先授权本服务团队");
} else {
const name = `${current.value.name} ${current.value.relationship ? `(${current.value.relationship})` : ""
}`;
const name = `${current.value.name} ${current.value.relationship || ""}`;
uni.navigateTo({
url: `/pages/health/list?teamId=${props.team.teamId}&corpId=${props.corpId
}&id=${current.value._id}&name=${encodeURIComponent(name)}`,
@ -156,17 +161,20 @@ function toggle(i) {
}
function toManagePage() {
const corpUserId = props.corpUserIds && props.corpUserIds[props.team.teamId] ? props.corpUserIds[props.team.teamId] : "";
uni.navigateTo({
url: `/pages/archive/archive-manage?corpId=${props.corpId}&teamId=${props.team.teamId}`,
url: `/pages/archive/archive-manage?corpUserId=${corpUserId}&corpId=${props.corpId}&teamId=${props.team.teamId}`,
});
}
async function auth() {
await confirm(`是否授权${props.team.name}提供服务`);
const corpUserId = await getResponsiblePerson();
const res = await api("authCustomerToTeam", {
corpId: props.corpId,
teamId: props.team.teamId,
id: current.value._id,
corpUserId
});
if (res && res.success) {
await toast("授权成功");
@ -177,10 +185,12 @@ async function auth() {
}
async function getCustomers() {
if (loading) return;
loading = true
const res = await api("getMiniAppCustomers", {
miniAppId: account.value.openid,
corpId: props.corpId,
});
}, false);
if (res && res.success) {
customers.value = res && Array.isArray(res.data) ? res.data : [];
const customer = customers.value.find(
@ -192,6 +202,16 @@ async function getCustomers() {
} else {
toast(res.message || "获取档案失败");
}
loading = false
}
async function getResponsiblePerson() {
if (!externalUserId.value) {
await getExternalUserId(props.corpId)
}
const corpUserId = props.corpUserIds && props.corpUserIds[props.team.teamId] ? props.corpUserIds[props.team.teamId] : "";
const res = await api('getResponsiblePerson', { corpId: props.corpId, teamId: props.team.teamId, corpUserId, externalUserId: externalUserId.value });
return res && res.data ? res.data : ''
}
onMounted(() => {
@ -209,6 +229,10 @@ watch(
},
{ immediate: true }
);
defineExpose({
getCustomers,
})
</script>
<style scoped>
.archive-container {

View File

@ -5,10 +5,11 @@
<team-head :team="team" :teams="teams" @changeTeam="changeTeam" />
</template>
<view class="home-section home-section--first">
<customer-archive :corpId="corpId" :team="team" @update:customers="handleCustomersUpdate" />
<customer-archive ref="archiveRef" :corpId="corpId" :corpUserIds="corpUserIds" :team="team"
@update:customers="handleCustomersUpdate" />
</view>
<view class="home-section">
<consult :corpId="corpId" :teamId="team.teamId" :team="team" :customers="customers" />
<consult ref="consultRef" :corpId="corpId" :teamId="team.teamId" :team="team" :customers="customers" />
</view>
<!-- <view class="home-section">
<team-mate :team="team" />
@ -46,6 +47,9 @@ const team = ref(null);
const teams = ref([]);
const loading = ref(false);
const customers = ref([]);
const consultRef = ref(null);
const archiveRef = ref(null);
const corpUserIds = ref({});
const corpId = computed(() => team.value?.corpId);
@ -69,15 +73,14 @@ async function changeTeam({ teamId, corpId, corpName }) {
}
}
async function getTeams() {
async function getTeams(inviteTeamId = '') {
loading.value = true;
const res = await api('getWxappRelateTeams', { openid: account.value.openid });
teams.value = res && Array.isArray(res.data) ? res.data : [];
const matchTeamId = get('home-invite-teamId') || (team.value ? team.value.teamId : '');
const matchTeamId = inviteTeamId || (team.value ? team.value.teamId : '');
const validTeam = teams.value.find(i => i.teamId && i.teamId === matchTeamId);
const firstTeam = teams.value[0]
if (validTeam || firstTeam) {
remove('home-invite-teamId');
changeTeam(validTeam || firstTeam);
} else {
team.value = null;
@ -97,12 +100,24 @@ onLoad(() => {
onShow(async () => {
if (!account.value) await login();
const inviteTeam = get('home-invite-team-info');
remove('home-invite-team-info');
if (inviteTeam && inviteTeam.teamId && inviteTeam.corpUserId) {
corpUserIds.value[inviteTeam.teamId] = inviteTeam.corpUserId;
}
if (account.value && account.value.openid) {
getTeams();
getTeams(inviteTeam && inviteTeam.teamId ? inviteTeam.teamId : '');
} else {
teams.value = [];
}
if (consultRef.value && typeof consultRef.value.getBadgeCount === 'function') {
consultRef.value.getBadgeCount()
}
if (archiveRef.value && typeof archiveRef.value.getCustomers === 'function') {
archiveRef.value.getCustomers()
}
});
watch(account, (n, o) => {
if (n && !o) {
getTeams();

View File

@ -110,7 +110,6 @@ async function bindTeam() {
}
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
if (res1 && res1.data > 0) {
set('home-invite-teamId', team.value.teamId);
toHome();
} else {
attempToPage(redirectUrl.value)
@ -143,7 +142,7 @@ async function attempToPage(url) {
onLoad((opts) => {
if (opts.source === "teamInvite") {
team.value = get("invite-team-info");
redirectUrl.value = `/pages/archive/edit-archive?teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
redirectUrl.value = `/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
return;
}
redirectUrl.value = opts.redirectUrl || "";

View File

@ -1,5 +1,5 @@
<template>
<view class="flex flex-col justify-center items-center h-full">
<view class="flex flex-col justify-center items-center h-full" @click="copy()">
<image class="flash-logo" src="/static/logo-plain.png" />
</view>
</template>
@ -19,18 +19,30 @@ const { account } = storeToRefs(useAccountStore());
const loading = ref(false);
const team = ref(null);
const opts = ref('');
async function changeTeam({ teamId, corpId }) {
function copy() {
uni.setClipboardData({
data: opts.value || '暂无内容'
})
}
async function changeTeam({ teamId, corpId, corpUserId }) {
loading.value = true;
const res = await api("getTeamData", { teamId, corpId, withCorpName: true });
loading.value = false;
if (res && res.data) {
team.value = res.data;
team.value.corpName = res.data.corpName;
set('home-invite-team-info', {
teamId: team.value.teamId,
corpUserId: corpUserId || ''
});
if (account.value) {
bindTeam()
bindTeam(corpUserId)
} else {
set("invite-team-info", {
corpUserId,
corpId: team.value.corpId,
teamId: team.value.teamId,
corpName: team.value.corpName,
@ -49,25 +61,25 @@ async function changeTeam({ teamId, corpId }) {
}
}
async function bindTeam() {
async function bindTeam(corpUserId) {
const res = await api('bindWxappWithTeam', { appid, corpId: team.value.corpId, teamId: team.value.teamId, openid: account.value.openid });
if (!res || !res.success) {
return toast("关联团队失败");
}
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
if (res1 && res1.data > 0) {
set('home-invite-teamId', team.value.teamId);
uni.switchTab({
url: "/pages/home/home",
});
} else {
uni.redirectTo({
url: `/pages/archive/edit-archive?teamId=${team.value.teamId}&corpId=${team.value.corpId}`
url: `/pages/archive/edit-archive?corpUserId=${corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`
})
}
}
onLoad((options) => {
opts.value = JSON.stringify(options)
const href =
typeof options.q === "string" ? decodeURIComponent(options.q) : "";
const [, url = ""] = href.split("?");

View File

@ -407,7 +407,7 @@ $primary-color: #0877F1;
display: flex;
align-items: center;
padding: 12rpx 20rpx;
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: 40rpx;
gap: 12rpx;
}
@ -514,7 +514,7 @@ $primary-color: #0877F1;
background: #fff;
border-top: 1rpx solid #eee;
padding: 20rpx 0 20rpx 60rpx;
padding-bottom: env(safe-area-inset-bottom);
padding-bottom: 40rpx;
gap: 40rpx 50rpx;
flex-wrap: wrap;
background-color: #f5f5f5;
@ -544,7 +544,7 @@ $primary-color: #0877F1;
background-color: white;
border-top: 1rpx solid #e0e0e0;
padding: 16rpx;
margin-bottom: env(safe-area-inset-bottom);
margin-bottom: 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
@ -689,7 +689,7 @@ $primary-color: #0877F1;
width: 100%;
max-height: 80vh;
padding: 20rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
padding-bottom: calc(20rpx + 40rpx);
position: relative;
box-sizing: border-box;
margin: 0;
@ -821,7 +821,7 @@ $primary-color: #0877F1;
background-color: white;
width: auto;
padding: 32rpx 20rpx 48rpx 20rpx;
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
padding-bottom: calc(48rpx + 40rpx);
text-align: center;
margin: 0 auto;
position: relative;

View File

@ -36,6 +36,8 @@ const handleApply = () => {
padding: 20rpx 32rpx;
background-color: #f5f5f5;
box-sizing: border-box;
position:absolute;
bottom: 40rpx;
}
.apply-card {

View File

@ -12,11 +12,10 @@
</template>
<script setup>
const emit = defineEmits(['cancel']);
const emit = defineEmits(["cancel"]);
const handleCancel = () => {
emit('cancel');
emit("cancel");
};
</script>
@ -26,6 +25,8 @@ const handleCancel = () => {
padding: 20rpx 32rpx;
background-color: #f5f5f5;
box-sizing: border-box;
position: absolute;
bottom: 40rpx;
}
.cancel-card {

View File

@ -195,9 +195,9 @@ const getArticleData = (message) => {
//
const handleArticleClick = (message) => {
const { articleId } = getArticleData(message);
const { articleId, sendId } = getArticleData(message);
uni.navigateTo({
url: `/pages/article/article-detail?id=${articleId}&corpId=${props.corpId}`,
url: `/pages/article/article-detail?id=${articleId}&sendId=${sendId || ''}&corpId=${props.corpId}`,
});
};
@ -207,6 +207,7 @@ const getSurveyData = (message) => {
if (message.payload && message.payload.data) {
const data = JSON.parse(message.payload.data);
return {
...data,
title: data.title || "填写问卷",
desc: data.desc || "请填写问卷",
url: data.url || "",
@ -221,18 +222,26 @@ const getSurveyData = (message) => {
desc: "请填写问卷",
url: "",
imgUrl: "",
err: true
};
};
//
const handleSurveyClick = (message) => {
const surveyData = getSurveyData(message);
if (surveyData.url) {
//
console.log("打开问卷:", surveyData.url);
// uni.navigateTo({
// url: `/pages/survey/fill?url=${encodeURIComponent(surveyData.url)}`
// });
if (surveyData.err) {
return
}
if (surveyData.isSystem) {
uni.navigateTo({
url: `/pages/web-view/web-view?src=${encodeURIComponent(surveyData.url)}`
})
return
}
if (surveyData.answerId) {
uni.navigateTo({
url: `/pages/survey/fill?name=${surveyData.name}&memberId=${surveyData.memberId}&surveryId=${surveyData.surveryId}&corpId=${surveyData.corpId}&answerId=${surveyData.answerId}`
})
}
};
</script>

View File

@ -58,8 +58,8 @@ export default function useGroupChat(groupID) {
return member.isTeamMember ? '/static/default-avatar.png' : '/static/default-patient-avatar.png'
}
// 获取群聊信息和成员头像
async function getGroupInfo() {
const gid = typeof groupID === 'string' ? groupID : groupID.value
async function getGroupInfo(id) {
const gid = id || groupID;
if (!gid) return
try {
@ -73,10 +73,8 @@ export default function useGroupChat(groupID) {
status: groupResult.data.orderStatus || 'active',
teamId: groupResult.data.teamId
}
// 2. 如果有teamId获取团队成员头像和名称
if (groupResult.data.teamId) {
const memberMap = await teamStore.getTeamMemberAvatarsAndName(groupResult.data.teamId)
// 3. 存储团队成员ID列表
@ -112,13 +110,14 @@ export default function useGroupChat(groupID) {
}
}
onShow(() => {
getGroupInfo()
})
// onShow(() => {
onUnload(() => {
// 清理资源
})
// getGroupInfo()
// })
// onUnload(() => {
// // 清理资源
// })
return {
groupInfo,

View File

@ -444,7 +444,7 @@ function getBubbleClass(message) {
}
//
onLoad((options) => {
onLoad(async (options) => {
groupId.value = options.groupID || "";
messageList.value = [];
isLoading.value = false;
@ -456,7 +456,7 @@ onLoad((options) => {
if (options.userID) {
chatInfo.value.userID = options.userID;
}
await getGroupInfo(groupId.value);
//
uni.onKeyboardHeightChange((res) => {
console.log("键盘高度变化:", res.height);
@ -473,6 +473,8 @@ onLoad((options) => {
}
});
// chatMember
checkLoginAndInitTIM();
updateNavigationTitle();
});
@ -563,7 +565,7 @@ const initTIMCallbacks = async () => {
.then(async () => {
console.log("✓ 收到新消息后已标记为已读");
// tabBar
await globalUnreadListenerManager.refreshBadge();
// await globalUnreadListenerManager.refreshBadge();
})
.catch((error) => {
console.error("✗ 标记已读失败:", error);
@ -717,7 +719,7 @@ const loadMessageList = async () => {
.then(async () => {
console.log("✓ 会话已标记为已读:", chatInfo.value.conversationID);
// tabBar
await globalUnreadListenerManager.refreshBadge();
// await globalUnreadListenerManager.refreshBadge();
})
.catch((error) => {
console.error("✗ 标记会话已读失败:", error);
@ -913,10 +915,10 @@ onHide(() => {
timChatManager.currentConversationID = null;
console.log("✓ 页面隐藏已清空当前会话ID");
// tabBar
if (globalUnreadListenerManager.isInitialized) {
globalUnreadListenerManager.refreshBadge();
}
// // tabBar
// if (globalUnreadListenerManager.isInitialized) {
// globalUnreadListenerManager.refreshBadge();
// }
});
//

View File

@ -380,8 +380,8 @@ const setupConversationListener = () => {
//
//
if (globalUnreadListenerManager.isInitialized) {
globalUnreadListenerManager.addCallback("onConversationListUpdated", handleConversationListUpdate);
globalUnreadListenerManager.addCallback("onMessageReceived", handleMessageReceived);
// globalUnreadListenerManager.addCallback("onConversationListUpdated", handleConversationListUpdate);
// globalUnreadListenerManager.addCallback("onMessageReceived", handleMessageReceived);
console.log("【消息列表页】已添加回调到全局监听器回调链");
} else {
console.warn("【消息列表页】全局未读监听器未初始化,使用直接回调方式");
@ -454,7 +454,7 @@ const handleClickConversation = async (conversation) => {
console.log("✓ 已标记会话为已读:", conversation.conversationID);
// tabBar
await globalUnreadListenerManager.refreshBadge();
// await globalUnreadListenerManager.refreshBadge();
} catch (error) {
console.error("✗ 标记会话已读失败:", error);
}
@ -514,9 +514,9 @@ const cleanMessageText = (text) => {
//
onShow(async () => {
// tabBar
if (globalUnreadListenerManager.isInitialized) {
await globalUnreadListenerManager.refreshBadge();
}
// if (globalUnreadListenerManager.isInitialized) {
// await globalUnreadListenerManager.refreshBadge();
// }
});
//
@ -544,12 +544,12 @@ onUnmounted(() => {
updateTimer = null;
}
//
if (globalUnreadListenerManager.isInitialized) {
globalUnreadListenerManager.removeCallback("onConversationListUpdated", handleConversationListUpdate);
globalUnreadListenerManager.removeCallback("onMessageReceived", handleMessageReceived);
console.log("【消息列表页】已从回调链移除回调");
}
// //
// if (globalUnreadListenerManager.isInitialized) {
// globalUnreadListenerManager.removeCallback("onConversationListUpdated", handleConversationListUpdate);
// globalUnreadListenerManager.removeCallback("onMessageReceived", handleMessageReceived);
// console.log("");
// }
});
</script>

251
pages/rate/rate-detail.vue Normal file
View File

@ -0,0 +1,251 @@
<template>
<full-page v-if="record">
<view class="flex items-center px-15 py-10 border-b">
<view>
<image class="box-50" :src="record.avatar"></image>
</view>
<view class="ml-10">
<view class="text-base font-semibold leading-normal">
{{ record.userName || "" }}
</view>
<view class="text-sm text-gray">{{ record.job }}</view>
</view>
</view>
<view class="px-15 py-10">
<view class="text-base mb-10">请您对我的本次服务进行评价</view>
<view class="flex items-center px-10 py-5 mb-10 border rounded">
<view v-for="i in 5" :key="i" class="mr-5 w-star" @click="changeRate(i)">
<uni-icons v-if="i <= starCount" type="star-filled" :size="30" color="#FF9900"></uni-icons>
<uni-icons v-else type="star" :size="30" color="#FF9900"></uni-icons>
</view>
<view class="flex-shrink-0 w-star text-sm text-primary whitespace-nowrap">
{{ rateText }}
</view>
</view>
<template v-if="enable">
<view class="flex flex-wrap text-sm">
<view v-for="tag in tags" :key="tag._id"
class="py-5 px-10 mr-10 mb-10 max-w-full break-all rounded-full bg-gray border" :class="selectedTag[tag._id]
? 'text-white bg-primary border-primary'
: ''
" @click="toggle(tag)">
{{ tag.text }}
</view>
</view>
<view class="px-10 py-5 border rounded">
<textarea v-model="words" :auto-height="true" class="text-base block w-full min-h-100"
placeholder="展开说说您对我本次服务的想法吧……" placeholder-class="text-gray text-base" :maxlength="1000" />
<view class="text-gray text-right">
{{ words.length }} / {{ 1000 }}
</view>
</view>
</template>
<template v-else>
<view v-if="record.tags && record.tags.length" class="flex flex-wrap text-sm">
<view v-for="(tag, i) in record.tags" :key="i"
class="py-5 px-10 mr-10 mb-10 max-w-full break-all rounded-full text-white bg-primary border-primary border-[1px] border-solid">
{{ tag }}
</view>
</view>
<view v-if="record.words && record.words.length"
class="px-10 py-5 border border-solid border-gray-100 rounded pointer-events-none">
<textarea :value="record.words" :disabled="true" class="text-sm block w-full min-h-[100px]"
placeholder-class="text-gray text-sm" :maxlength="1000" />
</view>
</template>
<view class="h-20"></view>
</view>
<template #footer>
<view v-if="enable" class="flex-shrink-0 px-15 py-10 text-center bg-white">
<view class="pb-10 text-sm">
{{ record.userName ? `您正在对 ${record.userName} 进行匿名评价` : "您正在进行匿名评价" }}
</view>
<view class="py-10 text-base text-white rounded bg-primary" @click="confirm()">
提交
</view>
</view>
</template>
</full-page>
<view v-else class="h-screen relative">
<view v-if="isError" class="empty">
<image class="empty__icon" src="/static/empty.svg"></image>
<text class="empty__txt">{{ emptyTxt }} {{ record }}</text>
</view>
<view v-else-if="rated" class="empty text-green-500">
<uni-icons type="checkbox-filled" color=" " size="60"></uni-icons>
<text class="text-gray text-base block mt-4">评价已完成</text>
</view>
<view v-else class="empty text-gray">
<!-- <view class="animate-spin ease-linear duration-[2500]">
<uni-icons type="spinner-cycle animate-spin" color=" " size="50"></uni-icons>
</view> -->
<text class="text-base block mt-4">评价尚未完成</text>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue';
import { onLoad } from "@dcloudio/uni-app";
import api from "@/utils/api.js";
import { toast } from "@/utils/widget";
import FullPage from '@/components/full-page.vue';
// import { submitRateRecord, getRateRecord } from "@/api/knowledgeBase.js";
const RateText = {
1: "很不满意",
2: "不满意",
3: "一般",
4: "满意",
5: "很满意",
};
const RateStar = {
1: "oneStar",
2: "twoStar",
3: "threeStar",
4: "fourStar",
5: "fiveStar",
};
const id = ref("");
const corpId = ref("");
const isError = ref(true);
const record = ref(null);
const enable = ref(false);
const rateTags = ref([]);
const rated = ref(false);
const emptyTxt = ref("正在获取评价信息...");
const rate = ref(0);
const words = ref("");
const selectedTag = ref({});
const loading = ref(false);
const tags = computed(() => {
if (enable.value) {
const key = RateStar[rate.value];
const group = rateTags.value.find((i) => i.rateStar === key);
return group && Array.isArray(group.rateTags) ? group.rateTags : [];
}
return [];
});
const starCount = computed(() => {
return rate.value % 1 === 0 && rate.value >= 0 && rate.value <= 5
? rate.value
: 0;
})
const emptyStarCount = computed(() => {
return 5 - starCount.value;
});
const rateText = computed(() => {
return RateText[starCount.value] || "";
});
async function confirm() {
if (!enable.value) return;
if (![1, 2, 3, 4, 5].includes(rate.value)) {
toast("请选择评分");
return;
}
const tagsText = tags.value
.filter((i) => selectedTag.value[i._id])
.map((i) => i.text);
loading.value = true;
const { message, success } = await api('submitRateRecord', {
id: id.value,
corpId: corpId.value,
rate: rate.value,
tags: tagsText,
words: words.value,
});
if (success) {
await toast("评价成功");
uni.navigateBack();
} else {
toast(message);
loading.value = false;
}
}
async function init() {
const res = await api('getRateRecord', {
id: id.value,
corpId: corpId.value,
});
if (res && res.success) {
enable.value = typeof res.enable == 'boolean' ? res.enable : false;
rated.value = typeof res.rated == 'boolean' ? res.rated : false;
record.value = res.record;
rate.value = res.record && res.record.rate ? res.record.rate : 0;
rateTags.value = Array.isArray(res.rateTags) ? res.rateTags : [];
if (record.value && record.value.updateTime) {
words.value = typeof record.value.words == 'string' ? record.value.words : '';
} else if (record.value && Date.now() > record.value.expireTime) {
rate.value = 5
}
}
}
function changeRate(i) {
if (!enable.value) return;
rate.value = i;
selectedTag.value = {};
}
function toggle(i) {
if (!enable.value) return;
selectedTag.value[i._id] = !selectedTag.value[i._id];
}
onLoad(opts => {
corpId.value = opts.corpId;
id.value = opts.id;
if (id.value && corpId.value) {
init();
}
})
</script>
<style scoped lang="scss">
.h-screen {
height: 100vh;
}
.min-h-100 {
min-height: 200rpx;
}
.box-50 {
width: 100rpx;
height: 100rpx;
}
.w-star {
width: 16%;
}
.empty {
position: absolute;
left: 50%;
top: 40%;
transform: translate(-50%, -50%);
text-align: center;
@at-root &__icon {
display: block;
margin-bottom: 16rpx;
width: 240rpx;
height: 240rpx;
}
@at-root &__txt {
font-size: 32rpx;
color: #666;
}
}
</style>

441
pages/rate/rate-list.vue Normal file
View File

@ -0,0 +1,441 @@
<template>
<full-page @reachBottom="getMore">
<template #header>
<scroll-view scroll-x class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full"
:show-scrollbar="false">
<view v-for="(tab, index) in tabs" :key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors" :class="[
activeTab === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
: 'bg-white text-gray-600 border-gray-200'
]" @click="selectTab(tab.value)">
{{ tab.name }}
</view>
</scroll-view>
</template>
<view class="bg-gray-100 min-h-screen">
<!-- Survey List -->
<view v-if="loading && list.length === 0" class="loading-container">
<uni-icons type="spinner-cycle" size="30" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="!loading && list.length === 0" class="empty-container">
<empty-data text="暂无评价" />
</view>
<view v-else class="p-15">
<view v-for="item in list" :key="item._id" class="bg-white rounded-lg p-15 mb-15 shadow-sm"
@click="goToDetail(item)">
<!-- Header -->
<view class="flex items-start justify-between mb-10">
<view class="flex items-start flex-1 mr-10 relative">
<view class="text-xs text-green-600 border border-green-600 px-5 rounded mr-5 flex-shrink-0 mt-1 tag-box">
服务评价
</view>
<view class="text-base font-bold text-gray-800 leading-normal line-clamp-2">
请您对我的今日服务做出评价
</view>
</view>
<view class="flex items-center flex-shrink-0 ml-2">
<text v-if="item.rate" class="text-sm mr-2 text-gray-400">查看</text>
<text v-else class="text-sm mr-2 text-danger">未评价</text>
<uni-icons type="right" size="14" color="#9ca3af"></uni-icons>
</view>
</view>
<view class="text-sm text-gray-600 mb-5 flex items-start">
<text class="text-gray-400 mr-2 field-label">服务人员:</text>
<text>{{ item.userName || '-' }}</text>
</view>
<view class="text-sm text-gray-600 mb-10 pb-10 border-b border-gray-100 flex items-start">
<text class="text-gray-400 mr-2 field-label">团队:</text>
<text>{{ item.teamName || '-' }}</text>
</view>
<view class="flex items-center justify-between">
<view class="text-sm text-gray-400">
服务时间: {{ item.time || '-' }}
</view>
<view class="text-sm text-gray-400">
人员: {{ item.customerName || '-' }}
</view>
</view>
</view>
<view v-if="loading && list.length > 0" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999" />
<text>加载中...</text>
</view>
<view v-if="!hasMore" class="no-more">
没有更多了
</view>
</view>
</view>
</full-page>
</template>
<script setup>
import { ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import dayjs from "dayjs";
import api from "@/utils/api.js";
import usePageList from '@/hooks/usePageList';
import useAccountStore from "@/store/account.js";
import EmptyData from "@/components/empty-data.vue";
import FullPage from '@/components/full-page.vue';
const { openid } = storeToRefs(useAccountStore());
const tabs = ref([{ name: "全部", value: "" }]);
const activeTab = ref("");
const corpId = ref('')
const teamId = ref('')
const inited = ref(false);
const { list, page, pages, pageSize, total, loading, hasMore, changePage } = usePageList(getList)
const selectTab = async (memberId) => {
if (activeTab.value === memberId) return;
activeTab.value = memberId;
changePage(1)
};
const loadCustomers = async () => {
const miniAppId = openid.value || uni.getStorageSync("openid");
if (!miniAppId) return;
try {
const res = await api("getMiniAppCustomers", { miniAppId, corpId: corpId.value });
if (res && res.success) {
const list = Array.isArray(res.data) ? res.data : [];
tabs.value = [
{ name: "全部", value: "" },
...list.map((c) => ({ name: c.name || "未命名", value: c._id })),
];
} else {
uni.showToast({ title: res?.message || "获取档案失败", icon: "none" });
}
} catch (err) {
console.error("loadCustomers failed:", err);
uni.showToast({ title: "获取档案失败", icon: "none" });
}
};
async function getList() {
if (loading.value) return;
const customerIds = activeTab.value ? [activeTab.value] : tabs.value.map(i => i.value);
const res = await api('searchRateList', {
corpId: corpId.value,
page: page.value,
pageSize: pageSize.value,
customerIds,
teamId: teamId.value
})
const arr = res && Array.isArray(res.list) ? res.list.map(i => ({
...i,
time: i.createTime ? dayjs(i.createTime).format('YYYY-MM-DD HH:mm') : '-',
})) : [];
list.value = page.value === 1 ? arr : [...list.value, ...arr];
total.value = res && typeof res.total === 'number' ? res.total : 0;
pages.value = res && typeof res.pages === 'number' ? res.pages : 0;
};
function goToDetail(item) {
uni.navigateTo({
url: `/pages/rate/rate-detail?id=${item._id}&corpId=${item.corpId}`
})
}
function getMore() {
if (hasMore.value && !loading.value) {
changePage(page.value + 1)
}
}
onLoad(opts => {
corpId.value = opts.corpId
teamId.value = opts.teamId
})
onShow(async () => {
if (!inited.value) {
await loadCustomers();
inited.value = true
}
await changePage(1)
});
</script>
<style scoped>
.min-h-screen {
min-height: 100vh;
}
.bg-gray-100 {
background-color: #f7f8fa;
}
.bg-white {
background-color: #ffffff;
}
.p-15 {
padding: 30rpx;
}
.px-15 {
padding-left: 30rpx;
padding-right: 30rpx;
}
.py-10 {
padding-top: 20rpx;
padding-bottom: 20rpx;
}
.py-5 {
padding-top: 10rpx;
padding-bottom: 10rpx;
}
.px-5 {
padding-left: 10rpx;
padding-right: 10rpx;
}
.mr-10 {
margin-right: 20rpx;
}
.mr-5 {
margin-right: 10rpx;
}
.ml-2 {
margin-left: 10rpx;
}
.mr-2 {
margin-right: 10rpx;
}
.mb-15 {
margin-bottom: 30rpx;
}
.mb-10 {
margin-bottom: 20rpx;
}
.mb-5 {
margin-bottom: 10rpx;
}
.mt-1 {
margin-top: 6rpx;
}
.pb-10 {
padding-bottom: 20rpx;
}
.flex {
display: flex;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.flex-1 {
flex: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.relative {
position: relative;
}
.border {
border-width: 1px;
border-style: solid;
}
.border-b {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.rounded-full {
border-radius: 9999px;
}
.rounded {
border-radius: 8rpx;
}
.rounded-lg {
border-radius: 12rpx;
}
.shadow-sm {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.text-xs {
font-size: 22rpx;
}
.text-sm {
font-size: 28rpx;
}
.text-base {
font-size: 32rpx;
}
.font-bold {
font-weight: 600;
}
.leading-normal {
line-height: 1.4;
}
/* Colors - Adjusting to match image roughly */
.text-orange-500 {
color: #f29e38;
}
.bg-orange-100 {
background-color: #fff8eb;
}
.border-orange-500 {
border-color: #f29e38;
}
.text-gray-600 {
color: #333333;
}
.text-gray-500 {
color: #999999;
}
.text-gray-400 {
color: #999999;
}
.text-gray-800 {
color: #1a1a1a;
}
.border-gray-200 {
border-color: #e5e5e5;
}
.border-gray-100 {
border-color: #f5f5f5;
}
.text-green-600 {
color: #4b8d5f;
}
.border-green-600 {
border-color: #4b8d5f;
}
.text-red-500 {
color: #e04a4a;
}
.sticky {
position: sticky;
}
.top-0 {
top: 0;
}
.z-10 {
z-index: 10;
}
.w-full {
width: 100%;
}
.whitespace-nowrap {
white-space: nowrap;
}
.inline-block {
display: inline-block;
}
.tag-box {
border-radius: 4rpx;
line-height: 1.2;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
word-break: break-all;
}
.field-label {
flex-shrink: 0;
white-space: nowrap;
}
.loading-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
.loading-more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #999;
gap: 10rpx;
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<view class="h-full py-20vh">
<view class="mb-10 px-15 text-lg font-semibold text-center">{{ survey.name }}</view>
<scroll-view :scroll-y="true" class="h-40vh">
<view class="text-sm text-gray px-15 text-center leading-normal">
{{ survey.description || '' }}
</view>
</scroll-view>
<view v-if="customerName" class="my-4 text-center text-base">
<text class="text-gray">客户</text>
<text class="font-semibold">{{ customerName }}</text>
</view>
<view
class="mx-auto answer-btn h-12 text-base flex items-center justify-center text-white text-center rounded-full bg-primary"
@click="answer()">
开始答题
</view>
</view>
</template>
<script setup>
defineProps({
customerName: { type: String, default: '' },
survey: { type: Object, default: () => ({}) }
})
const emits = defineEmits(['answer'])
function answer() {
emits('answer')
}
</script>
<style>
.h-40vh {
height: 40vh;
}
.py-20vh {
padding: 20vh 0;
}
.my-4 {
margin-top: 30rpx;
margin-bottom: 30rpx;
}
.answer-btn {
width: 80%;
}
.h-12 {
height: 96rpx;
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<view class="h-full flex flex-col">
<view class="flex-shrink-0 px-10 pt-5 text-lg text-primary font-semibold text-center truncate">{{ survey.name }}
</view>
<view v-if="survey.description"
class="flex-shrink-0 px-10 mt-10 text-sm leading-normal text-gray line-clamp-2 text-center">
{{ survey.description }}
</view>
<view v-if="survey.enableScore" class="text-right mt-10 px-10 text-sm">
当前得分<span class="text-primary text-base font-semibold min-w-5 inline-block">{{ allScore }} </span>
</view>
<view v-if="quesiton" class="my-3 text-base font-semibold px-10 leading-normal ">
<span v-if="quesiton.require" class="text-xs text-danger">*</span> {{ index + 1 }}{{ quesiton.title }}
</view>
<view v-if="quesiton" class="flex-grow relative">
<scroll-view :scroll-y="true" class="absolute inset-0">
<view class="px-10">
<template v-if="quesiton.type === 'radio'">
<view v-for="(opt, idx) in quesiton.options" :key="opt.value" class="flex py-4 border-b "
:class="idx === 0 ? 'border-t' : ''" @click="changeRadio(opt.value)">
<uni-icons v-if="answer[quesiton.id] === opt.value" class="mr-2 text-primary flex-shrink-0" color=" "
type="checkbox-filled" size="24"></uni-icons>
<uni-icons v-else class="mr-2 text-gray flex-shrink-0" color=" " type="circle" size="24"></uni-icons>
<view class="flex-grow text-base leading-normal">
{{ opt.label }}
<span v-if="survey.enableScore && opt.score >= 0">
{{ opt.score }}
</span>
</view>
</view>
</template>
<textarea v-else-if="quesiton.type === 'input'" :value="answer[quesiton.id]" placeholder="请输入..."
placeholder-class="text-gray text-sm"
class="p-10 text-sm w-full box-border rounded border min-h-30 border-gray-200" @input="change($event)" />
</view>
</scroll-view>
</view>
<view v-if="list.length > 1" class="flex-shrink-0 py-3">
<view class="mx-3 bg-gray-100 h-3 rounded-lg overflow-hidden">
<view class="h-3 rounded-lg bg-primary" :style="{ width: `${progress}%` }"></view>
</view>
<view class="flex items-center justify-between px-10 py-15">
<view class="flex items-center text-primary" :class="index > 0 ? '' : 'opacity-0'" @click="prev()">
<uni-icons class="inline-block mr-10rpx" color=" " type="arrowleft"></uni-icons>
<span class="text-base font-semibold">上一题</span>
</view>
<!-- 当前得分40 -->
<view class="text-sm">{{ index + 1 }} / {{ list.length }}</view>
<view v-if="index < list.length - 1" class="flex items-center text-primary"
:class="index < list.length - 1 ? '' : 'opacity-0'" @click="next()">
<span class="text-base font-semibold">下一题</span>
<uni-icons class="inline-block ml-10rpx" color=" " type="arrowright"></uni-icons>
</view>
<view v-if="index === list.length - 1" class="flex items-center text-primary" @click="submit()">
<span class="text-base font-semibold">提交</span>
</view>
</view>
</view>
<view v-if="list.length === 1"
class="flex-shrink-0 mx-10 py-12 leading-normal text-white text-center rounded bg-primary" @click="submit()">
提交
</view>
</view>
</template>
<script>
import { toast } from '@/utils/widget'
export default {
name: 'Question',
props: {
list: { type: Array, default: () => ([]) },
survey: { type: Object, default: () => ({}) }
},
data() {
return {
index: 0,
answer: {},
waiting: false
}
},
computed: {
quesiton() {
return this.list[this.index]
},
progress() {
return (this.index + 1) / (this.list.length || 1) * 100
},
allScore() {
return this.list.reduce((score, item) => {
if (item.type === 'radio') {
const opt = item.options.find(i => i.value && i.value === this.answer[item.id])
if (opt && opt.score >= 0) {
score = Math.floor(score * 100 + opt.score * 100) / 100
}
}
return score
}, 0)
}
},
methods: {
change(e) {
this.$set(this.answer, this.quesiton.id, e.detail.value)
},
changeIndex(num) {
if (this.waiting) return;
this.waiting = true;
setTimeout(() => this.waiting = false, 1000);
const target = num + this.index;
this.index = target >= 0 && target < this.list.length ? target : this.index
},
changeRadio(value) {
this.$set(this.answer, this.quesiton.id, value)
},
prev() {
this.changeIndex(-1)
},
next() {
if (this.quesiton.require && this.quesiton.type === 'radio' && !this.quesiton.options.some(i => i.value && i.value === this.answer[this.quesiton.id])) {
toast('请完成当前题目')
} else if (this.quesiton.require && this.quesiton.type === 'input' && (typeof this.answer[this.quesiton.id] !== 'string' || this.answer[this.quesiton.id].trim() === '')) {
toast('请完成当前题目')
} else {
this.changeIndex(1)
}
},
verify() {
const index = this.list.findIndex(i => {
if (i.require && i.type === 'radio' && !i.options.some(j => j.value && j.value === this.answer[i.id])) {
return true
}
if (i.require && i.type === 'input' && (typeof this.answer[i.id] !== 'string' || this.answer[i.id].trim() === '')) {
return true
}
return false
})
if (index >= 0) {
toast('请完成第' + (index + 1) + '题')
this.index = index
return false
}
return true
},
submit() {
if (this.verify()) {
const list = this.list.map(i => {
const item = {
id: i.id,
value: this.answer[i.id] || '',
title: i.title,
type: i.type,
require: i.require,
}
if (Array.isArray(i.options)) {
item.options = i.options.map(opt => ({ ...opt }))
}
return item
})
this.$emit('submit', { list, score: this.allScore })
}
}
}
}
</script>
<style lang="scss" scoped>
.pt-5 {
padding-top: 40rpx;
}
.min-w-5 {
min-width: 40rpx;
}
.inline-block {
display: inline-block;
}
.my-3 {
margin-top: 24rpx;
margin-bottom: 24rpx;
}
.py-4 {
padding-top: 30rpx;
padding-bottom: 30rpx;
}
.border-t {
border-top: 1px solid #eee;
}
.min-h-30 {
min-height: 200rpx;
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<view class="h-full flex flex-col">
<view class="flex-shrink-0 px-10 pt-5 text-base font-semibold text-center truncate">{{ surery.name }}</view>
<view v-if="surery.description"
class="flex-shrink-0 px-10 mt-10 text-sm leading-normal text-gray line-clamp-2 text-center">
{{ surery.description }}
</view>
<view class="flex items-center text-gray text-sm px-10 mt-10">
<view v-if="surery.enableScore">
当前得分<text class="text-primary text-base font-semibold min-w-5 inline-block">{{ allScore }}
</text>
</view>
<view class="ml-auto">
客户<text class="text-black text-base font-semibold min-w-5 inline-block">{{ customerName }} </text>
</view>
</view>
<view class="flex-grow relative">
<scroll-view :scroll-y="true" class="absolute inset-0">
<view v-for="(quesiton, index) in list" :key="quesiton.id">
<view v-if="quesiton" class="my-4 text-base px-10 leading-normal">
<text v-if="quesiton.require" class="text-base text-red-500">*</text> {{ index + 1 }}{{ quesiton.title }}
</view>
<view class="px-10">
<template v-if="quesiton.type === 'radio'">
<view v-for="(opt, idx) in quesiton.options" :key="opt.value" class="flex py-15 border-b"
:class="idx === 0 ? 'border-t' : ''">
<view class="flex-grow pl-4 text-gray leading-normal"
:class="quesiton.value && quesiton.value === opt.value ? 'text-primary' : ''">
{{ opt.label }}
<text v-if="surery.enableScore && opt.score >= 0">
{{ opt.score }}
</text>
</view>
<uni-icons v-if="quesiton.value && quesiton.value === opt.value" class="ml-1 text-primary flex-shrink-0"
color=" " type="checkbox-filled" size="24"></uni-icons>
</view>
</template>
<view v-else-if="quesiton.type === 'input'"
class="my-4 p-10 text-base leading-normal border rounded">
{{ quesiton.value || '' }}
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
name: 'Question',
props: {
customerName: { type: String, default: '' },
list: { type: Array, default: () => ([]) },
surery: { type: Object, default: () => ({}) }
},
computed: {
allScore() {
return this.list.reduce((score, item) => {
if (item.type === 'radio') {
const opt = item.options.find(i => i.value && i.value === item.value && item.value)
if (opt && opt.score >= 0) {
score = Math.floor(score * 100 + opt.score * 100) / 100
}
}
return score
}, 0)
}
}
}
</script>
<style lang="scss" scoped>
.min-w-5 {
min-width: 40rpx
}
.pt-5 {
padding-top: 40rpx;
}
.my-4 {
margin-top: 32rpx;
margin-bottom: 32rpx;
}
.border-t {
border-top: 1px solid #eee;
}
.pl-4 {
padding-left: 30rpx;
}
</style>

243
pages/survey/fill.vue Normal file
View File

@ -0,0 +1,243 @@
<template>
<full-page :customScroll="true">
<template v-if="survey && survey.status === 'enable' && !readonly">
<survey-cover v-if="step === 'cover'" :customerName="customerName" :survey="survey" @answer="step = 'question'" />
<survey-question v-else-if="step === 'question'" :survey="survey" :list="showList" @submit="submit" />
</template>
<survey-record v-else-if="survey && survey.status === 'enable' && readonly" :customerName="customerName"
:survey="survey" :list="showList" />
<view v-else class="empty">
<image class="empty__icon" src="/static/empty.svg"></image>
<text class="empty__txt">{{ emptyTxt }}</text>
</view>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import api from "@/utils/api.js";
import { loading, toast, hideLoading } from "@/utils/widget";
import FullPage from '@/components/full-page.vue';
import surveyCover from "./components/survey-cover.vue";
import surveyQuestion from "./components/survey-question.vue";
import surveyRecord from "./components/survey-record.vue";
const corpId = ref('');
const surveryId = ref('');
const answerId = ref('');
const memberId = ref('');
const survey = ref(null);
const emptyTxt = ref('')
const customerName = ref('');
const step = ref('cover');
const readonly = computed(() => survey.value && survey.value.submitTime);
const list = computed(() => survey.value && Array.isArray(survey.value.list) ? survey.value.list : []);
const showList = computed(() => {
return list.value.filter(item => {
const showCase = Array.isArray(item.showCase) ? item.showCase : [];
if (showCase.length) {
const res = showCase.every(({
type,
id,
value
}) => {
const question = this.list.find(i => i.id === id);
const val = question ? question.value : '';
return type === 'in' ? value.includes(val) : val === value.join()
})
return res
}
return true
})
})
async function init() {
loading('加载中...');
try {
const record = await getAnswerRecord();
if (record && record.submitTime) {
record.status = 'enable';
survey.value = record;
hideLoading()
return;
}
survey.value = await getSurvey();
if (survey.value.status == 'stop') {
emptyTxt.value = '问卷已经停用'
} else if (survey.value.status == 'init') {
emptyTxt.value = '问卷暂未启用'
}
} catch (e) {
hideLoading();
toast('获取问卷失败');
uni.navigateBack();
}
}
async function getAnswerRecord() {
const res = await api('getAnswer', { corpId: corpId.value, surveryId: surveryId.value, answerId: answerId.value, memberId: memberId.value });
if (res && res.success) {
return res.record
}
return Promise.reject();
}
async function getSurvey() {
const res = await api('getSurveryDetail', { corpId: corpId.value, id: surveryId.value })
if (res && res.success && res.data) {
return res.data
}
return Promise.reject();
}
async function submit({ list, score }) {
const res = await api('answerSurvery', {
corpId: corpId.value,
name: survey.value.name,
memberId: memberId.value,
list: list,
score: score,
answerId: answerId.value,
})
if (res && res.success) {
await toast('提交成功');
uni.navigateBack();
}
}
onLoad(opts => {
corpId.value = opts.corpId;
surveryId.value = opts.surveryId;
answerId.value = opts.answerId;
memberId.value = opts.memberId;
customerName.value = opts.name;
init();
})
</script>
<style scoped lang="scss">
.survey {
position: relative;
height: 100%;
width: 100%;
height: 100vh;
width: 100vw;
color: ragb(0, 0, 0, .9);
background: #fff;
@at-root &__wrapper {
padding: 30rpx 40rpx;
}
@at-root &__scroll {
height: 100%;
}
@at-root &__customer {
text-align: right;
font-size: 28rpx;
padding-bottom: 16rpx;
margin-bottom: 24rpx;
color: #333;
border-bottom: 1px solid #eee;
}
@at-root &__title {
text-align: center;
font-size: 32rpx;
font-weight: 600;
margin-bottom: 24rpx;
}
@at-root &__desc {
text-align: left;
color: #666;
font-size: 28rpx;
margin-bottom: 24rpx;
}
@at-root &__item {
padding: 0 10rpx;
margin-bottom: 24rpx;
}
@at-root &__question {
position: relative;
font-size: 32rpx;
margin-bottom: 24rpx;
@at-root &--require::before {
position: absolute;
left: -20rpx;
top: 0;
content: '*';
color: #f56c6c;
}
}
@at-root &__input {
padding: 10rpx 24rpx;
font-size: 28rpx;
border: 1px solid #eee;
border-radius: 8rpx;
}
@at-root &__radio {
display: flex;
align-items: center;
padding-bottom: 24rpx;
cursor: pointer;
}
@at-root &__btn {
margin-top: 30rpx;
padding: 24rpx 30rpx;
border-radius: 8rpx;
font-size: 28rpx;
color: #fff;
background: #006eff;
text-align: center;
}
}
.radio {
@at-root &__icon {
flex-shrink: 0;
margin-top: 4rpx;
margin-right: 10rpx;
width: 40rpx;
height: 40rpx;
}
@at-root &__label {
flex-grow: 1;
font-size: 32rpx;
}
}
.empty {
position: absolute;
left: 50%;
top: 40%;
transform: translate(-50%, -50%);
text-align: center;
@at-root &__icon {
display: block;
margin-bottom: 16rpx;
width: 240rpx;
height: 240rpx;
}
@at-root &__txt {
font-size: 32rpx;
color: #666;
}
}
</style>

View File

@ -1,17 +1,18 @@
<template>
<view class="bg-gray-100 min-h-screen">
<!-- Filter Tabs -->
<scroll-view scroll-x class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full"
:show-scrollbar="false">
<view v-for="(tab, index) in tabs" :key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors" :class="[
activeTab === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
: 'bg-white text-gray-600 border-gray-200'
]" @click="selectTab(tab.value)">
{{ tab.name }}
</view>
</scroll-view>
<full-page pageClass="bg-gray-100">
<template #header>
<scroll-view scroll-x class="bg-white whitespace-nowrap px-15 py-10 sticky top-0 z-10 w-full"
:show-scrollbar="false">
<view v-for="(tab, index) in tabs" :key="index"
class="inline-block px-15 py-5 mr-10 text-sm rounded-full border transition-colors" :class="[
activeTab === tab.value
? 'bg-orange-100 text-orange-500 border-orange-500'
: 'bg-white text-gray-600 border-gray-200'
]" @click="selectTab(tab.value)">
{{ tab.name }}
</view>
</scroll-view>
</template>
<!-- Survey List -->
<view v-if="loading && surveys.length === 0" class="loading-container">
@ -41,7 +42,8 @@
<!-- Status (暂不展示未填写/已填写) -->
<view class="flex items-center flex-shrink-0 ml-2">
<text class="text-sm mr-2 text-gray-400">查看</text>
<text v-if="item.status === 'FILLED'" class="text-sm mr-2 text-gray-400">查看</text>
<text v-else class="text-sm mr-2 text-danger">未填写</text>
<uni-icons type="right" size="14" color="#9ca3af"></uni-icons>
</view>
</view>
@ -72,7 +74,7 @@
没有更多了
</view>
</view>
</view>
</full-page>
</template>
<script setup>
@ -82,6 +84,8 @@ import { storeToRefs } from "pinia";
import dayjs from "dayjs";
import api from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import fullPage from "@/components/full-page.vue";
import EmptyData from "@/components/empty-data.vue";
@ -89,7 +93,8 @@ const { openid } = storeToRefs(useAccountStore());
const tabs = ref([{ name: "全部", value: "" }]);
const activeTab = ref("");
const corpId = ref('')
const corpId = ref('');
const teamId = ref('');
const surveys = ref([]);
const total = ref(0);
@ -134,6 +139,8 @@ const mapRowToView = (row) => {
team: row?.team?.name || "-",
time: sendTime,
status: row?.status || "",
memberId: row?.memberId || "",
corpId: row?.corpId || "",
};
};
@ -156,6 +163,7 @@ const loadSurveyList = async (reset = false) => {
const params = {
corpId: corpId.value,
miniAppId,
teamId: teamId.value,
page: page.value,
pageSize,
};
@ -179,13 +187,17 @@ const loadSurveyList = async (reset = false) => {
}
};
function goToDetail() {
uni.showToast({ title: "详情暂未接入", icon: "none" });
function goToDetail(item) {
console.log(item)
uni.navigateTo({
url: `/pages/survey/fill?name=${item.person}&memberId=${item.memberId}&surveryId=${item.surveryId}&corpId=${item.corpId}&answerId=${item._id}`
})
}
onLoad(opts => {
corpId.value = opts.corpId
corpId.value = opts.corpId;
teamId.value = opts.teamId;
})
onShow(async () => {

View File

@ -11,7 +11,9 @@
</view> -->
<view class="flex">
<view class="flex-shrink-0 text-base text-gray">执业机构</view>
<view v-if="member.hospitalName" class="flex-shrink-0 text-base text-dark">{{ member.hospitalName }}</view>
<view v-if="member.source === 'wxapp'" class="flex-shrink-0 text-base text-dark">
{{ member.hospitalName || '' }}
</view>
<view v-else class="flex-shrink-0 text-base text-dark">{{ corpNames }}</view>
</view>
</view>

View File

@ -79,7 +79,7 @@ export default defineStore("accountStore", () => {
console.log('IM 初始化成功');
// IM 初始化成功后,设置全局未读消息监听
globalUnreadListenerManager.setup();
// globalUnreadListenerManager.setup();
return true;
} catch (error) {
@ -99,7 +99,7 @@ export default defineStore("accountStore", () => {
// 清除全局未读监听
if (globalUnreadListenerManager.isInitialized) {
globalUnreadListenerManager.destroy();
// globalUnreadListenerManager.destroy();
}
} catch (error) {
console.error('退出腾讯IM失败:', error);

View File

@ -40,10 +40,10 @@ export default defineStore("teamStore", () => {
// 获取团队成员头像和名称映射
async function getTeamMemberAvatarsAndName(teamId) {
if (!teamId || !account.value?.corpId) return {};
if (!teamId) return {};
const res = await api('getTeamMemberAvatarsAndName', {
teamId,
corpId: account.value.corpId
teamId
}, false);
if (res && res.success && res.data) {
return res.data;

View File

@ -12,7 +12,9 @@ const urlsConfig = {
getCorpMemberJob: "getCorpMemberJob",
bindWxappWithTeam: 'bindWxappWithTeam',
getWxappRelateTeams: 'getWxappRelateTeams',
getTeamMemberAvatarsAndName: "getTeamMemberAvatarsAndName"
getTeamMemberAvatarsAndName: "getTeamMemberAvatarsAndName",
getMiniAppHomeStats: "getMiniAppHomeStats",
getResponsiblePerson: 'getTeamResponsiblePerson'
},
knowledgeBase: {
@ -36,7 +38,10 @@ const urlsConfig = {
addArticleSendRecord: 'addArticleSendRecord',
addArticleReadRecord: 'addArticleReadRecord',
getMiniAppReceivedArticleList: 'getMiniAppReceivedArticleList',
getPageDisease:'getPageDisease'
getPageDisease: 'getPageDisease',
searchRateList: 'searchRateList',
submitRateRecord: 'submitRateRecord',
getRateRecord: 'getRateRecord',
},
member: {
addCustomer: 'add',
@ -56,7 +61,7 @@ const urlsConfig = {
updateCustomer: 'update'
},
wecom: {
addContactWay: 'addContactWay'
addContactWay: 'getCorpFriendQrcode'
},
im: {
getUserSig: 'getUserSig',
@ -70,7 +75,11 @@ const urlsConfig = {
getGroupList: "getGroupList"
},
survery: {
getMiniAppReceivedSurveryList: 'getMiniAppReceivedSurveryList'
getMiniAppReceivedSurveryList: 'getMiniAppReceivedSurveryList',
getSurveryDetail: 'getDetail',
answerSurvery: 'answerSurvery',
getAnswerRecord: 'getAnswerRecord',
getAnswer: 'getAnswer'
}
}
const urls = Object.keys(urlsConfig).reduce((acc, path) => {