From e35bd4ab490a6b5b1abc189a5b79d16c8f4bbf50 Mon Sep 17 00:00:00 2001 From: zjwmiao <1723168479@qq.com> Date: Fri, 14 Feb 2025 16:03:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0cookie=E5=90=8C?= =?UTF-8?q?=E6=84=8F=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components.d.ts | 1 + package.json | 2 + pnpm-lock.yaml | 17 ++ src/App.vue | 2 + src/assets/svg-icons/icon-close.svg | 3 + src/components/CookieNotice.vue | 394 ++++++++++++++++++++++++++++ src/components/hooks/useScreen.ts | 159 +++++++++++ src/i18n/cookie/cookie-en.ts | 17 ++ src/i18n/cookie/cookie-zh.ts | 17 ++ src/i18n/cookie/index.ts | 7 + src/i18n/index.ts | 3 + src/main.ts | 4 - src/routers/index.ts | 14 +- src/shared/analytics.ts | 46 +++- src/shared/utils.ts | 34 +++ src/stores/cookies.ts | 44 ++++ 16 files changed, 746 insertions(+), 18 deletions(-) create mode 100644 src/assets/svg-icons/icon-close.svg create mode 100644 src/components/CookieNotice.vue create mode 100644 src/components/hooks/useScreen.ts create mode 100644 src/i18n/cookie/cookie-en.ts create mode 100644 src/i18n/cookie/cookie-zh.ts create mode 100644 src/i18n/cookie/index.ts create mode 100644 src/stores/cookies.ts diff --git a/components.d.ts b/components.d.ts index 0af5c05..8502682 100644 --- a/components.d.ts +++ b/components.d.ts @@ -14,6 +14,7 @@ declare module 'vue' { AppIssue: typeof import('./src/components/AppIssue.vue')['default'] AppPull: typeof import('./src/components/AppPull.vue')['default'] AppVerify: typeof import('./src/components/AppVerify.vue')['default'] + CookieNotice: typeof import('./src/components/CookieNotice.vue')['default'] DocAnchor: typeof import('./src/components/DocAnchor.vue')['default'] ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] diff --git a/package.json b/package.json index ea45d87..eabf78a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "axios": "^1.6.8", "element-plus": "^2.3.4", "js-base64": "^3.7.5", + "js-cookie": "^3.0.5", "lodash-es": "^4.17.21", "opendesign": "link:opendesign", "pinia": "^2.1.6", @@ -36,6 +37,7 @@ "vue-router": "^4.3.0" }, "devDependencies": { + "@types/js-cookie": "^3.0.6", "@types/lodash-es": "^4.17.7", "@types/node": "18.16.19", "@types/prismjs": "^1.26.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 990be72..27a3e52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: js-base64: specifier: ^3.7.5 version: 3.7.5 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -53,6 +56,9 @@ importers: specifier: ^4.3.0 version: 4.3.0(vue@3.3.4) devDependencies: + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/lodash-es': specifier: ^4.17.7 version: 4.17.7 @@ -438,6 +444,9 @@ packages: '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/json-schema@7.0.11': resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} @@ -1743,6 +1752,10 @@ packages: js-base64@3.7.5: resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3184,6 +3197,8 @@ snapshots: '@types/minimatch': 5.1.2 '@types/node': 18.16.19 + '@types/js-cookie@3.0.6': {} + '@types/json-schema@7.0.11': {} '@types/lodash-es@4.17.7': @@ -4579,6 +4594,8 @@ snapshots: js-base64@3.7.5: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: diff --git a/src/App.vue b/src/App.vue index 2dd05ae..9711bfe 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,6 +10,7 @@ import AppHeader from '@/components/AppHeader.vue'; import AppFooter from '@/components/AppFooter.vue'; import { refreshInfo } from '@/shared/login'; +import CookieNotice from './components/CookieNotice.vue'; refreshInfo(); const { locale } = useI18n(); @@ -31,6 +32,7 @@ watch( diff --git a/src/assets/svg-icons/icon-close.svg b/src/assets/svg-icons/icon-close.svg new file mode 100644 index 0000000..2a02c9a --- /dev/null +++ b/src/assets/svg-icons/icon-close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/CookieNotice.vue b/src/components/CookieNotice.vue new file mode 100644 index 0000000..f1376ba --- /dev/null +++ b/src/components/CookieNotice.vue @@ -0,0 +1,394 @@ + + + + + diff --git a/src/components/hooks/useScreen.ts b/src/components/hooks/useScreen.ts new file mode 100644 index 0000000..25e579d --- /dev/null +++ b/src/components/hooks/useScreen.ts @@ -0,0 +1,159 @@ +import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'; + +export enum Size { + Phone = 'phone', + PadV = 'pad_v', + PadH = 'pad_h', + Laptop = 'laptop', +} + +export type ScreenSizeT = + | typeof Size.Phone + | Size.PadV + | Size.PadH + | Size.Laptop; + +export const ScreenConfig = { + [Size.Phone]: 600, + [Size.PadV]: 840, + [Size.PadH]: 1200, + [Size.Laptop]: 1440, +}; + +/** + * lt: less than, 小于 < + * le: less than or equal to, 小于等于 <= + * eq: equal to, 等于 = + * ne: never equal to, 不等于 != + * ge: greater than or equal to, 大于等于 >= + * gt: greater than, 大于 > + */ +export type CompareT = 'lt' | 'le' | 'eq' | 'ne' | 'ge' | 'gt'; + +const CompareHandler = { + lt: (a: number, b: number) => a < b, + le: (a: number, b: number) => a <= b, + eq: (a: number, b: number) => a === b, + ne: (a: number, b: number) => a !== b, + ge: (a: number, b: number) => a >= b, + gt: (a: number, b: number) => a > b, +}; + +export const useScreen = () => { + const screenSize = reactive({ + width: 1440, + height: 0, + }); + + const current = ref(Size.Laptop); + + const getSize = (width = screenSize.width) => { + if (width < ScreenConfig[Size.Phone]) { + return Size.Phone; + } else if (width < ScreenConfig[Size.PadV]) { + return Size.PadV; + } else if (width < ScreenConfig[Size.PadH]) { + return Size.PadH; + } else { + return Size.Laptop; + } + }; + + const compare = (type: CompareT = 'eq', size: ScreenSizeT) => { + const w1 = screenSize.width; + const w2 = ScreenConfig[size]; + const handler = CompareHandler[type]; + return handler(w1, w2); + }; + + /** + * phone + */ + const isPhone = computed(() => compare('le', Size.Phone)); // [0, 600] + const gtPhone = computed(() => compare('gt', Size.Phone)); // [601, -] + + /** + * pad + */ + const isPad = computed( + () => compare('gt', Size.Phone) && compare('le', Size.PadH) + ); // [601, 1200] + const lePad = computed(() => compare('le', Size.PadH)); // [0, 1200] + const gtPad = computed(() => compare('gt', Size.PadH)); // [1201, -] + + /** + * pad_v + */ + const isPadV = computed( + () => compare('gt', Size.Phone) && compare('le', Size.PadV) + ); // [601, 840] + const lePadV = computed(() => compare('le', Size.PadV)); // [0, 840] + const gtPadV = computed(() => compare('gt', Size.PadV)); // [841, -] + + /** + * pad_h + */ + const isPadH = computed( + () => compare('gt', Size.PadV) && compare('le', Size.PadH) + ); // [841, 1200] + + /** + * laptop + */ + const isLaptop = computed( + () => compare('gt', Size.PadH) && compare('le', Size.Laptop) + ); // [1201, 1440] + const leLaptop = computed(() => compare('le', Size.Laptop)); // [0, 1440] + const gtLaptop = computed(() => compare('gt', Size.Laptop)); // [1441, -] + const isPadToLaptop = computed( + () => compare('gt', Size.Phone) && compare('le', Size.Laptop) + ); // [601, 1440] + const isPadVToLaptop = computed( + () => compare('gt', Size.PadV) && compare('le', Size.Laptop) + ); // [841, 1440] + + const onWindowResize = () => { + const { innerWidth, innerHeight } = window; + screenSize.width = innerWidth; + screenSize.height = innerHeight; + current.value = getSize(); + }; + + onMounted(() => { + window.addEventListener('resize', onWindowResize); + onWindowResize(); + nextTick(() => onWindowResize()); + }); + + onUnmounted(() => { + window.removeEventListener('resize', onWindowResize); + }); + + return { + // 获取屏幕宽度分级 + getSize, + // 当前屏幕分级 + current, + // 当前屏幕宽度 + size: screenSize, + + isPhone, // [0, 600] + gtPhone, // [601, -] + + isPad, // [601, 1200] + lePad, // [0, 1200] + gtPad, // [1201, -] + + isPadV, // [601, 840] + lePadV, // [0, 840] + gtPadV, // [841, -] + + isPadH, // [841, 1200] + + isLaptop, // [1201, 1440] + leLaptop, // [0, 1440] + gtLaptop, // [1441, -] + isPadToLaptop, // [601, 1440] + isPadVToLaptop, // [841, 1440] + }; +}; diff --git a/src/i18n/cookie/cookie-en.ts b/src/i18n/cookie/cookie-en.ts new file mode 100644 index 0000000..716aceb --- /dev/null +++ b/src/i18n/cookie/cookie-en.ts @@ -0,0 +1,17 @@ +export default { + title: 'openEuler Community Respects Your Privacy.', + desc: 'his site uses cookies from us and our partners to improve your browsing experience and make the site work properly. By clicking "Accept All", you consent to the use of cookies. By clicking "Reject All", you disable the use of unnecessary cookies. You can manage your cookie settings by clicking "Manage Cookies". For more information or to change your cookie settings, please refer to our ', + link: 'About Cookies', + acceptAll: 'Accept All', + rejectAll: 'Reject All', + manage: 'Manage Cookies', + necessaryCookie: 'Strictly Necessary Cookies', + necessaryCookieTip: 'Always active', + necessaryCookieDetail: + 'These cookies are necessary for the site to work properly and cannot be switched off. They are usually only set in response to actions made by you which amount to a request for services, such as logging in or filling in forms. You can set the browser to block these cookies, but that can make parts of the site not work. These cookies do not store any personally identifiable information.', + analyticalCookie: 'Analytics Cookies', + analyticalCookieDetail: + 'We will use these cookies only with your consent. These cookies help us make improvements by collecting statistics such as the number of visits and traffic sources.', + saveSetting: 'Save and Accept', + allowAll: 'Accept All', +}; diff --git a/src/i18n/cookie/cookie-zh.ts b/src/i18n/cookie/cookie-zh.ts new file mode 100644 index 0000000..d9d1c58 --- /dev/null +++ b/src/i18n/cookie/cookie-zh.ts @@ -0,0 +1,17 @@ +export default { + title: 'openEuler社区重视您的隐私', + desc: '我们在本网站上使用Cookie,包括第三方Cookie,以便网站正常运行和提升浏览体验。单击“全部接受”即表示您同意这些目的;单击“全部拒绝”即表示您拒绝非必要的Cookie;单击“管理Cookie”以选择接受或拒绝某些Cookie。需要了解更多信息或随时更改您的Cookie首选项,请参阅我们的', + link: '《关于cookies》', + acceptAll: '全部接受', + rejectAll: '全部拒绝', + manage: '管理Cookie', + necessaryCookie: '必要Cookie', + necessaryCookieTip: '始终启用', + necessaryCookieDetail: + '这些Cookie是网站正常工作所必需的,不能在我们的系统中关闭。它们通常仅是为了响应您的服务请求而设置的,例如登录或填写表单。您可以将浏览器设置为阻止Cookie来拒绝这些Cookie,但网站的某些部分将无法正常工作。这些Cookie不存储任何个人身份信息。', + analyticalCookie: '统计分析Cookie', + analyticalCookieDetail: + '我们将根据您的同意使用和处理这些非必要Cookie。这些Cookie允许我们获得摘要统计数据,例如,统计访问量和访问者来源,便于我们改进我们的网站。', + saveSetting: '保存并接受', + allowAll: '全部接受', +}; diff --git a/src/i18n/cookie/index.ts b/src/i18n/cookie/index.ts new file mode 100644 index 0000000..a92f0d2 --- /dev/null +++ b/src/i18n/cookie/index.ts @@ -0,0 +1,7 @@ +import zh from './cookie-zh'; +import en from './cookie-en'; + +export default { + zh, + en, +}; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 5262970..3d99b4a 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -4,17 +4,20 @@ import sig from './sig'; import common from './common'; import quickIssue from './quick-issue'; +import cookie from './cookie'; const messages = { zh: { common: common.zh, sig: sig.zh, quickIssue: quickIssue.zh, + cookie: cookie.zh, }, en: { common: common.en, sig: sig.en, quickIssue: quickIssue.en, + cookie: cookie.en, }, }; diff --git a/src/main.ts b/src/main.ts index 7911140..9272923 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,6 @@ import Prism from 'prismjs'; import App from './App.vue'; import OpenDesign from 'opendesign'; -import { enableOA, reportPerformance } from './shared/analytics'; VueMarkdownEditor.use(vuepressTheme, { Prism, @@ -26,7 +25,4 @@ app.use(OpenDesign); app.use(router); app.use(VueMarkdownEditor); -enableOA(); -reportPerformance(); - app.mount('#app'); diff --git a/src/routers/index.ts b/src/routers/index.ts index 5c26d57..d1c09e3 100644 --- a/src/routers/index.ts +++ b/src/routers/index.ts @@ -1,6 +1,5 @@ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { useLangStore } from '@/stores'; -import { reportPV } from '@/shared/analytics'; export const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/zh/issues' }, @@ -34,20 +33,9 @@ export const router = createRouter({ }, }); -router.beforeEach((to, from) => { +router.beforeEach((to) => { // 设置语言 const langStore = useLangStore(); const lang = to.fullPath.includes('en') ? 'en' : 'zh'; langStore.lang = lang; - - if (from.path === '/') { - return; - } - to.meta.$referrer = window.location.href; -}); - -router.afterEach((to, from) => { - if (to.path !== from.path) { - reportPV(to.meta.$referrer as string); - } }); diff --git a/src/shared/analytics.ts b/src/shared/analytics.ts index 62b553f..e7ee566 100644 --- a/src/shared/analytics.ts +++ b/src/shared/analytics.ts @@ -4,21 +4,62 @@ import { getClientInfo, } from '@opensig/open-analytics'; import { reportAnalytics } from '@/api/api-analytics'; +import { Router } from 'vue-router'; +import { COOKIE_AGREED_STATUS, useCookieStore } from '@/stores/cookies'; export const oa = new OpenAnalytics({ appKey: 'openEuler', request: (data) => { + if ( + useCookieStore().getUserCookieStatus() !== COOKIE_AGREED_STATUS.ALL_AGREED + ) { + disableOA(); + return; + } reportAnalytics(data); }, }); -export const enableOA = () => { +let routerGuards: (() => void)[]; + +export const enableOA = async (router: Router) => { + if (oa.enabled) { + return; + } oa.setHeader(getClientInfo()); oa.enableReporting(true); + reportPerformance(); + await router.isReady(); + reportPV(); + (routerGuards ??= []).push( + router.beforeEach((to, from) => { + if (from.path === '/' || to.path === from.path) { + return; + } + to.meta.$referrer = window.location.href; + }), + router.afterEach((to, from) => { + if (to.path === from.path) { + return; + } + reportPV(to.meta.$referrer as string); + }) + ); }; export const disableOA = () => { oa.enableReporting(false); + if (routerGuards) { + routerGuards.forEach((item) => item()); + routerGuards = []; + } + [ + 'oa-openEuler-client', + 'oa-openEuler-events', + 'oa-openEuler-session', + ].forEach((key) => { + localStorage.removeItem(key); + }); }; export const reportPV = ($referrer?: string) => { @@ -40,6 +81,9 @@ export const oaReport = >( eventOptions?: any; } ) => { + if (!oa.enabled) { + return; + } return oa.report( event, async (...opt) => ({ diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 1713fac..6bd6ca9 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -1,3 +1,5 @@ +import Cookies from 'js-cookie'; + // TS 对象key合法检查 export function isValidKey( key: string | number | symbol, @@ -27,3 +29,35 @@ export function getYearByOffset(offset = 8) { return utcTime.getFullYear(); } getYearByOffset(); + +/** + * 获取指定key的cookie值 + * @param key + * @returns 值 + */ +export function getCookie(key: string) { + return Cookies.get(key); +} + +/** + * 设置cookie + * @param key cookie的key + * @param value cookie的值 + * @param day cookie的过期时间 默认1天 + */ +export function setCookie( + key: string, + value: string, + day = 1, + domain: string = location.hostname +) { + Cookies.set(key, value, { expires: day, domain }); +} + +/** + * 删除cookie + * @param key cookie的key + */ +export function removeCookie(key: string) { + Cookies.remove(key); +} diff --git a/src/stores/cookies.ts b/src/stores/cookies.ts new file mode 100644 index 0000000..664872f --- /dev/null +++ b/src/stores/cookies.ts @@ -0,0 +1,44 @@ +import { getCookie } from '@/shared/utils'; +import { defineStore } from 'pinia'; + +export const COOKIE_AGREED_STATUS = { + NOT_SIGNED: '0', // 未签署 + ALL_AGREED: '1', // 同意所有cookie + NECCESSARY_AGREED: '2', // 仅同意必要cookie +}; + +export const COOKIE_KEY = 'agreed-cookiepolicy'; + +export const useCookieStore = defineStore('cookie', { + state: () => ({ + status: COOKIE_AGREED_STATUS.NOT_SIGNED, + version: '20240830', + }), + getters: { + isAllAgreed(state) { + return state.status === COOKIE_AGREED_STATUS.ALL_AGREED; + }, + }, + actions: { + getUserCookieStatus() { + const cookieVal = getCookie(COOKIE_KEY) ?? '0'; + const cookieStatusVal = cookieVal[0]; + const privacyVersionVal = cookieVal.slice(1); + if (privacyVersionVal !== this.version) { + this.status = COOKIE_AGREED_STATUS.NOT_SIGNED; + return COOKIE_AGREED_STATUS.NOT_SIGNED; + } + + if (cookieStatusVal === COOKIE_AGREED_STATUS.ALL_AGREED) { + this.status = COOKIE_AGREED_STATUS.ALL_AGREED; + return COOKIE_AGREED_STATUS.ALL_AGREED; + } else if (cookieStatusVal === COOKIE_AGREED_STATUS.NECCESSARY_AGREED) { + this.status = COOKIE_AGREED_STATUS.NECCESSARY_AGREED; + return COOKIE_AGREED_STATUS.NECCESSARY_AGREED; + } else { + this.status = COOKIE_AGREED_STATUS.NOT_SIGNED; + return COOKIE_AGREED_STATUS.NOT_SIGNED; + } + }, + }, +}); -- Gitee