feat(miniapp): 消息订阅设置页(模板 ID 占位,#11)
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
import { get, post } from './request';
|
||||||
|
|
||||||
|
// 订阅消息模板 ID 占位:申请到模板后替换为真实 ID
|
||||||
|
export const SUBSCRIBE_TEMPLATES: { key: string; label: string; id: string }[] = [
|
||||||
|
{ key: 'inspection', label: '巡检风险提醒', id: '' },
|
||||||
|
{ key: 'alarm', label: '告警通知', id: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface WechatBinding {
|
||||||
|
bound: boolean;
|
||||||
|
openId?: string;
|
||||||
|
authorized?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bindWechat = (code: string) => post<{ bound: boolean; openId: string }>('/wechat/bind', { code });
|
||||||
|
export const getWechatBinding = () => get<WechatBinding>('/wechat/binding');
|
||||||
|
export const updateWechatSubscribe = (template: string, authorized: boolean) =>
|
||||||
|
post<{ authorized: string[] }>('/wechat/subscribe', { template, authorized });
|
||||||
@@ -14,6 +14,7 @@ export default defineAppConfig({
|
|||||||
'pages/knowledge/index',
|
'pages/knowledge/index',
|
||||||
'pages/knowledge/detail/index',
|
'pages/knowledge/detail/index',
|
||||||
'pages/inspection/index',
|
'pages/inspection/index',
|
||||||
|
'pages/notification/index',
|
||||||
],
|
],
|
||||||
window: {
|
window: {
|
||||||
backgroundTextStyle: 'dark',
|
backgroundTextStyle: 'dark',
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '消息订阅',
|
||||||
|
});
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
@use '@/styles/variables.scss' as *;
|
||||||
|
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: $color-bg-page;
|
||||||
|
padding: $spacing-md $spacing-lg;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: $color-bg-card;
|
||||||
|
border-radius: $radius-lg;
|
||||||
|
padding: $spacing-lg;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardTitle {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-lg;
|
||||||
|
font-weight: $font-weight-semibold;
|
||||||
|
color: $color-text-primary;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardDesc {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
line-height: $line-height-normal;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusLabel {
|
||||||
|
font-size: $font-size-md;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusOk {
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusNo {
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
color: $color-text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openId {
|
||||||
|
display: block;
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
color: $color-text-tertiary;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: $spacing-sm 0 0;
|
||||||
|
@include button-reset;
|
||||||
|
background: $color-primary;
|
||||||
|
color: $color-text-white;
|
||||||
|
border-radius: $radius-button;
|
||||||
|
height: $button-height-md;
|
||||||
|
font-size: $font-size-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tplRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: $spacing-sm 0;
|
||||||
|
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tplLabel {
|
||||||
|
font-size: $font-size-md;
|
||||||
|
color: $color-text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tplReady {
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
color: $color-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tplPending {
|
||||||
|
font-size: $font-size-xs;
|
||||||
|
color: $color-warning;
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Button, Text, View } from '@tarojs/components';
|
||||||
|
import Taro from '@tarojs/taro';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
import {
|
||||||
|
SUBSCRIBE_TEMPLATES,
|
||||||
|
bindWechat,
|
||||||
|
getWechatBinding,
|
||||||
|
updateWechatSubscribe,
|
||||||
|
type WechatBinding,
|
||||||
|
} from '@/api/wechat';
|
||||||
|
|
||||||
|
const NotificationPage: React.FC = () => {
|
||||||
|
const [binding, setBinding] = useState<WechatBinding | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
const data = await getWechatBinding().catch(() => ({ bound: false } as WechatBinding));
|
||||||
|
setBinding(data);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
const handleBind = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { code } = await Taro.login();
|
||||||
|
if (!code) throw new Error('获取登录 code 失败');
|
||||||
|
await bindWechat(code);
|
||||||
|
Taro.showToast({ title: '微信绑定成功', icon: 'success' });
|
||||||
|
refresh();
|
||||||
|
} catch (err) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: err instanceof Error ? err.message : '绑定失败',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubscribe = async () => {
|
||||||
|
const configured = SUBSCRIBE_TEMPLATES.filter((t) => t.id);
|
||||||
|
if (configured.length === 0) {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '模板未配置',
|
||||||
|
content: '订阅消息模板 ID 尚未配置,申请模板后填入 miniapp/src/api/wechat.ts 即可启用。',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '知道了',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await Taro.requestSubscribeMessage({
|
||||||
|
tmplIds: configured.map((t) => t.id),
|
||||||
|
});
|
||||||
|
for (const t of configured) {
|
||||||
|
if (res[t.id] === 'accept') {
|
||||||
|
await updateWechatSubscribe(t.key, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Taro.showToast({ title: '订阅设置已更新', icon: 'success' });
|
||||||
|
refresh();
|
||||||
|
} catch (err) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: err instanceof Error ? err.message : '订阅失败',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className={styles.page}>
|
||||||
|
<View className={styles.card}>
|
||||||
|
<Text className={styles.cardTitle}>微信订阅消息</Text>
|
||||||
|
<Text className={styles.cardDesc}>
|
||||||
|
绑定微信后,巡检风险(黄/橙/红)和告警发生时可通过订阅消息提醒你。注意:微信一次性订阅消息每次触发都需重新授权。
|
||||||
|
</Text>
|
||||||
|
<View className={styles.statusRow}>
|
||||||
|
<Text className={styles.statusLabel}>绑定状态</Text>
|
||||||
|
<Text className={binding?.bound ? styles.statusOk : styles.statusNo}>
|
||||||
|
{binding?.bound ? '已绑定' : '未绑定'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{binding?.bound && binding.openId ? (
|
||||||
|
<Text className={styles.openId}>openid:{binding.openId}</Text>
|
||||||
|
) : null}
|
||||||
|
{binding?.bound && binding.authorized && binding.authorized.length > 0 ? (
|
||||||
|
<Text className={styles.openId}>已授权:{binding.authorized.join('、')}</Text>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
className={`${styles.btn} ${styles.btnPrimary}`}
|
||||||
|
loading={loading}
|
||||||
|
onClick={handleBind}
|
||||||
|
>
|
||||||
|
绑定微信
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={`${styles.btn} ${styles.btnPrimary}`}
|
||||||
|
loading={loading}
|
||||||
|
onClick={handleSubscribe}
|
||||||
|
>
|
||||||
|
授权订阅
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
<View className={styles.card}>
|
||||||
|
<Text className={styles.cardTitle}>可订阅场景</Text>
|
||||||
|
{SUBSCRIBE_TEMPLATES.map((t) => (
|
||||||
|
<View key={t.key} className={styles.tplRow}>
|
||||||
|
<Text className={styles.tplLabel}>{t.label}</Text>
|
||||||
|
<Text className={t.id ? styles.tplReady : styles.tplPending}>
|
||||||
|
{t.id ? '已配置' : '待配置模板 ID'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationPage;
|
||||||
@@ -53,6 +53,9 @@ const SettingsPage: React.FC = () => {
|
|||||||
case 'inspection':
|
case 'inspection':
|
||||||
Taro.navigateTo({ url: '/pages/inspection/index' });
|
Taro.navigateTo({ url: '/pages/inspection/index' });
|
||||||
break;
|
break;
|
||||||
|
case 'notification':
|
||||||
|
Taro.navigateTo({ url: '/pages/notification/index' });
|
||||||
|
break;
|
||||||
case 'about':
|
case 'about':
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
title: '关于',
|
title: '关于',
|
||||||
@@ -174,6 +177,11 @@ const SettingsPage: React.FC = () => {
|
|||||||
<Text className={styles.menuLabel}>拍照巡检</Text>
|
<Text className={styles.menuLabel}>拍照巡检</Text>
|
||||||
<Text className={styles.menuArrow}>›</Text>
|
<Text className={styles.menuArrow}>›</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View className={styles.menuItem} onClick={() => handleMenuTap('notification')}>
|
||||||
|
<Text className={styles.menuIcon}>🔔</Text>
|
||||||
|
<Text className={styles.menuLabel}>消息订阅</Text>
|
||||||
|
<Text className={styles.menuArrow}>›</Text>
|
||||||
|
</View>
|
||||||
<View className={styles.menuItem} onClick={() => handleMenuTap('wsStatus')}>
|
<View className={styles.menuItem} onClick={() => handleMenuTap('wsStatus')}>
|
||||||
<Text className={styles.menuIcon}>🔗</Text>
|
<Text className={styles.menuIcon}>🔗</Text>
|
||||||
<Text className={styles.menuLabel}>连接状态</Text>
|
<Text className={styles.menuLabel}>连接状态</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user